Skip to content

feat: add plugin runtime primitives and agent foundation hooks#190

Open
dgarson wants to merge 1329 commits intodgarson/forkfrom
dgarson/foundation-primitives-core
Open

feat: add plugin runtime primitives and agent foundation hooks#190
dgarson wants to merge 1329 commits intodgarson/forkfrom
dgarson/foundation-primitives-core

Conversation

@dgarson
Copy link
Owner

@dgarson dgarson commented Feb 28, 2026

Summary

  • Plugin runtime primitives: KV store, cron scheduler, and quota limit enforcement for plugin extensions
  • Extended hook/type surface: New hook points for agent/tool/routing interception (before_tool_call, before_message_route, subagent spawn gates, prompt sections)
  • Agent runtime config types: Diagnostic event schema and runtime config type definitions
  • Plugin-contributed prompt sections: Agents can now inject plugin-provided system prompt sections; adds session_score built-in tool
  • Tool adapter hooks: Before-tool-call hook dispatch with embedded runner improvements
  • Subagent spawn gate: Pre-spawn hook dispatch and cleanup-scope spawn gate for subagents
  • In-process plugin dispatch: Gateway plugin dispatch bypassing WebSocket overhead
  • Before-message-route hook: Fire before_message_route plugin hook in auto-reply dispatch

Notes

This PR contains only the core infrastructure changes. Extension implementations (ocx-*, inter-agent-mail, etc.) and skill updates will follow in a separate PR.

Test Plan

  • pnpm test passes
  • pnpm build passes
  • Plugin runtime primitives (KV, cron, quota) unit tests pass
  • Agent hook dispatch integration verified

joshavant and others added 30 commits February 26, 2026 14:47
restartGatewayProcessWithFreshPid() checks SUPERVISOR_HINT_ENV_VARS to
decide whether to let the supervisor handle the restart (mode=supervised)
or to fork a detached child (mode=spawned). The existing list only had
native launchd vars (LAUNCH_JOB_LABEL, LAUNCH_JOB_NAME) and systemd vars
(INVOCATION_ID, SYSTEMD_EXEC_PID, JOURNAL_STREAM).

macOS launchd does NOT automatically inject LAUNCH_JOB_LABEL into the
child environment. OpenClaw's own plist generator (buildServiceEnvironment
in service-env.ts) sets OPENCLAW_LAUNCHD_LABEL instead. So on stock macOS
LaunchAgent installs, isLikelySupervisedProcess() returned false, causing
the gateway to fork a detached child on SIGUSR1 restart. The original
process then exits, launchd sees its child died, respawns a new instance
which finds the orphan holding the port — infinite crash loop.

Fix: add OPENCLAW_LAUNCHD_LABEL, OPENCLAW_SYSTEMD_UNIT, and
OPENCLAW_SERVICE_MARKER to the supervisor hint list. These are set by
OpenClaw's own service environment builders for both launchd and systemd
and are the reliable supervised-mode signals.

Fixes openclaw#27605
…nchctl/systemctl

When the /restart command runs inside an embedded agent process (no
SIGUSR1 listener), it falls through to triggerOpenClawRestart() which
calls launchctl kickstart -k directly — bypassing the pre-restart port
cleanup added in openclaw#27013. If the gateway was started via TUI/CLI, the
orphaned process still holds the port and the new launchd instance
crash-loops.

Add synchronous stale-PID detection (lsof) and termination
(SIGTERM→SIGKILL) inside triggerOpenClawRestart() itself, so every
caller — including the embedded agent /restart path — gets port cleanup
before the service manager restart command fires.

Closes openclaw#26736

Made-with: Cursor
Refactor lane preview finalization into explicit branches so stop-created
previews never duplicate sends when edit fails.

Add Telegram dispatch regressions for:
- stop-created preview edit failure (no duplicate send)
- existing preview edit failure (fallback send preserved)
- missing message id after stop-created flush (fallback send)

Thanks @obviyus for the original preview-prime direction in openclaw#27449.

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
openclaw#27632)

Fix file upload 'Something went wrong' error by sending the invoke
acknowledgement before performing the file upload, rather than after.

Changes:
- Move invokeResponse to fire immediately upon receiving fileConsent/invoke
- Handle file upload asynchronously without blocking the response
- Update test to wait for async upload completion using vi.waitFor

This prevents Teams from timing out while waiting for the HTTP 200
acknowledgement during slow file uploads to OneDrive.

Fixes openclaw#27632
Update tests to properly wait for async file upload operations:
- Use vi.waitFor() to wait for async upload completion in success case
- Use vi.waitFor() to wait for error message in cross-conversation case
- Add setTimeout delay for decline case to ensure async handler completes
- Adjust assertion order to match new execution flow (invokeResponse first)

The tests were failing because the file upload now happens asynchronously
after sending the invokeResponse, so we need to explicitly wait for the
async operations to complete before making assertions.
Remove trailing whitespace to pass oxfmt format check.
steipete and others added 20 commits February 27, 2026 22:04
…back

Co-authored-by: Cathryn Lavery <cathryn@littlemight.com>
* agents: auto-discover Ollama models without API key

* tests: cover Ollama autodiscovery warning behavior
openclaw#28295) thanks @zhoulongchao77

Verified:
- pnpm build
- pnpm check
- pnpm test:macmini

Co-authored-by: zhoulongchao77 <65058500+zhoulongchao77@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
…#27884) (openclaw#27928) thanks @joelnishanth

Verified:
- pnpm build
- pnpm check
- pnpm test:macmini

Co-authored-by: joelnishanth <140015627+joelnishanth@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
…penclaw#28822)

Merged via squash.

Prepared head SHA: 83d4329
Co-authored-by: lailoo <20536249+lailoo@users.noreply.github.com>
Co-authored-by: velvet-shark <126378+velvet-shark@users.noreply.github.com>
Reviewed-by: @velvet-shark
…ions in feishu_doc (openclaw#20304)

Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
…I calls (openclaw#28907) thanks @Glucksberg

Verified:
- pnpm build
- pnpm check
- pnpm test:macmini

Co-authored-by: Glucksberg <80581902+Glucksberg@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
…penclaw#28269) thanks @Glucksberg

Verified:
- pnpm build
- pnpm check
- pnpm test:macmini

Co-authored-by: Glucksberg <80581902+Glucksberg@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
… / quota errors (openclaw#28494) thanks @guoqunabc

Verified:
- pnpm build
- pnpm check
- pnpm test:macmini

Co-authored-by: guoqunabc <9532020+guoqunabc@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
…ta limits)

Introduces three first-class runtime namespaces available to every plugin
via runtime.kv, runtime.cron, and runtime.quota.

KV store (runtime.kv): scoped key-value persistence backed by the agent
state directory. Plugins get full CRUD + TTL expiry without touching the
filesystem directly. Critical for extensions that need durable config or
cross-invocation memory (e.g. budget ledgers, mail stores, policy caches).

Cron scheduler (runtime.cron): declarative periodic tasks registered by
name. Runs inside the gateway process on the given cron expression;
scheduling survives gateway restarts because registrations are replayed
at startup. Enables extensions like the event-ledger retention sweep or
the observability health-check heartbeat.

Quota system (runtime.quota): per-scope token/request/cost counters with
configurable windows and soft/hard limits. Plugins can increment usage and
check headroom before expensive operations -- the budget-manager extension
is built entirely on top of this.

Type namespaces are extracted to dedicated files (types.kv.ts, types.cron.ts,
types.quota.ts, types.agents.ts, types.gateway.ts, types.sessions.ts) so the
main runtime/types.ts stays readable. All three implementations have unit tests.

pnpm-lock.yaml is updated for the otel/opentelemetry-api dependency pulled
in by the observability extension.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tion

Adds four new plugin type modules that define the full interception surface
plugins can register against:

types.hooks.primitives.ts -- before_agent_run and before_message_route hooks.
  before_agent_run fires before every agent turn and lets plugins inject
  extra context, modify the effective config, or abort the run with a
  structured response. before_message_route fires at inbound routing time so
  plugins can redirect or suppress messages before any agent is activated.

types.hooks.subagent-spawn.ts -- before_subagent_spawn hook. Called just
  before a subagent is forked, giving plugins the ability to deny, reroute,
  or annotate the spawn (e.g. budget checks, policy enforcement).

types.hooks.tool-mutation.ts -- after_tool_call hook. Called with the raw
  tool result; plugins can rewrite or enrich the result before the agent sees
  it (useful for observability spans, sanitization, or result caching).

types.prompt-sections.ts -- PromptSectionBuilder API. Plugins register named
  prompt sections with a slot (prepend / append / replace:*), a priority, and
  an optional condition predicate. The agent runner collects and injects them
  at system-prompt assembly time (see system-prompt.plugin-sections.ts).

hooks.ts is updated to dispatch the four new hook types through the plugin
registry. registry.ts gains registerHook, registerPromptSection, and the
corresponding query helpers. plugin-sdk/index.ts re-exports everything so
plugin authors get a single entry-point import.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
types.agents.ts defines per-agent runtime overrides (model selection,
temperature, max-tokens, tool allow/deny lists, sandbox mode) that can be
set in config and are propagated into the agent runner at invocation time.
These config knobs are what lets extensions like the routing-policy set the
effective model per request without touching user-visible config.

zod-schema.agent-runtime.ts provides the Zod schema used for safe parsing
of those overrides from YAML/JSON config files.

diagnostic-events.ts gains new event types -- session.score, tool.cost,
agent.budget_exceeded -- that the session-score tool, budget-manager, and
observability extension emit.  All new events are typed and carry a
structured payload so downstream consumers (OTel exporters, evaluators) can
deserialize them without parsing raw log strings.

agent-events.ts adds the AgentLifecycleEvent union used by the cleanup-hook
gate and subagent spawn hooks to carry before/after context consistently.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… built-in tool

system-prompt.plugin-sections.ts introduces collectPluginPromptSections(),
which queries the plugin registry for registered PromptSectionBuilders, runs
them with the current session context, and returns sorted+filtered sections
ready for injection. Sections support three slots: prepend (before the core
prompt), append (after it), and replace:<slot-name> (swap out a named block).
Only the highest-priority registration wins for replace slots; collisions are
logged as warnings. This is the mechanism that lets extensions like
ocx-orchestration inject a live sprint context or ocx-routing-policy inject
model instructions without forking the system-prompt builder.

system-prompt.ts is updated to accept pluginSections and splice them in at
the appropriate positions in the final assembled prompt. The change is purely
additive -- callers that pass no sections get the same output as before.

session-score-tool.ts adds the session_score built-in agent tool (tool #10).
Agents call it to emit a structured self-quality score (0.0-1.0) with a
rubric label, optional tags, and a free-text rationale note. The score is
forwarded to the diagnostic event pipeline as a session.score event, where
the observability extension can pick it up, tag it with OTel attributes, and
surface it in dashboards. No LLM call is made; the tool is a lightweight
event emitter so latency impact is negligible.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…unner improvements

pi-tool-definition-adapter.ts now wraps each tool's execute function to fire
the before_tool_call plugin hook before dispatch. This lets plugins (such as
ocx-budget-manager) intercept any tool call, inspect its arguments, and either
allow, modify, or deny it -- without the agent having any awareness. The
after_tool_call hook (types.hooks.tool-mutation.ts) is wired at the same
layer so result rewriting is symmetric.

pi-tools.before-tool-call.ts implements the dispatch logic: it walks the
hook registry, calls each registered handler in priority order, and surfaces
a combined allow/deny decision. An integration test covers the allow,
deny-with-message, and result-mutate paths end-to-end.

Embedded runner changes (pi-embedded-runner/run.ts, attempt.ts, types.ts):
  - pluginSections is threaded through RunAttemptContext so the assembled
    system prompt picks up plugin sections collected before the run starts.
  - compact.ts and tool-split.ts receive minor type-safety fixes exposed by
    the stricter RunAttemptContext shape.
  - system-prompt.ts in the runner now forwards pluginSections into the
    shared buildAgentSystemPrompt builder.

pi-embedded-subscribe.* gains the tool-call plumbing so subscribe-mode
sessions (used by background agents) apply the same before/after hooks.

sessions-spawn-tool.ts updates the spawn payload to include agentRuntimeConfig
so spawned subagents inherit the caller's model/tool overrides unless
explicitly overridden.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…subagents

subagent-spawn.ts now fires the before_subagent_spawn plugin hook before
forking. Any registered plugin can inspect the spawn parameters (requested
agent ID, tool payload, parent session context) and return deny with a
reason -- the spawn is aborted and the reason is surfaced to the calling
agent as a tool error. This is the enforcement point used by
ocx-budget-manager to block spawns when a session is over budget, and by
ocx-routing-policy to reroute spawns to a lower-cost model tier.

cleanup-hook-gate.ts introduces an AsyncLocalStorage-based scope flag.
When an agent's error-cleanup hook runs, it wraps its execution in
runInCleanupHookScope(). isInCleanupHookScope() is checked at spawn time:
if true, the spawn is rejected immediately without calling plugin hooks.
This prevents runaway spawn chains triggered inside cleanup logic -- an
important safety property when cleanup hooks themselves use agent tools.

Tests (cleanup-hook-gate.test.ts, cleanup-hook-spawn-gate.test.ts,
subagent-spawn.cleanup-gate.test.ts) verify that:
  - Normal spawns pass through all hook handlers.
  - A deny result from any hook aborts the spawn.
  - Spawns attempted inside a cleanup scope are rejected unconditionally.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
plugin-gateway-dispatch.ts introduces createPluginGatewayDispatcher(), which
gives plugins a direct function-call path into the gateway method handler map.
Because plugins run in-process and are fully trusted, they skip the
WebSocket/credential stack entirely: the dispatcher looks up the method in the
merged handler map, constructs a synthetic GatewayRequestContext (with a
'plugin:<pluginId>' identity for audit logging), and invokes the handler
directly. This makes plugin-to-gateway calls synchronous from the plugin's
perspective and eliminates round-trip latency for internal operations like
inter-agent-mail delivery or orchestration state mutations.

server.impl.ts wires up the dispatcher after the handler map is fully
assembled (core handlers + plugin-registered handlers are both present) and
passes it into the plugin runtime via setGatewayDispatcher. The ordering
matters: plugins that register gateway methods during startup will see those
methods reflected in their own dispatcher.

session-utils.types.ts and test-helpers.mocks.ts are updated to expose the
GatewayRequestContext shape used by the synthetic dispatcher so tests can
construct realistic contexts without reimplementing the type inline.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
resolve-route.ts now accepts an optional hookRunner so the routing layer can
dispatch the before_message_route hook without coupling to the plugin registry
directly. The hook receives the resolved agent route and lets plugins redirect
to a different agent, suppress the message entirely (returning a canned
response), or annotate the routing context with extra metadata carried into
the agent run.

auto-reply/dispatch.ts is updated to pass the hookRunner through to
resolveAgentRoute so that the hook fires at the right point in the pipeline:
after channel-specific routing (pairing, allowlists, guild/team bindings) is
resolved but before the agent runner is started. This ordering is important
because the hook sees the authoritative resolved agent ID, not an intermediate
candidate.

dispatch.test.ts adds coverage for the suppress and redirect paths to ensure
a plugin returning a deny result terminates dispatch without starting an
agent run.

channel-plugins.ts is added to test-utils to provide a reusable channel
plugin fixture that registers mock hook handlers, reducing boilerplate in
routing and dispatch tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 31de6b7b8e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +65 to +66
const result = await spawnSubagentDirect(
{

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Route ACP sessions_spawn requests to ACP runtime

This change makes sessions_spawn always call spawnSubagentDirect, so calls that still pass runtime: "acp" (including current system-prompt guidance in src/agents/system-prompt.ts) are silently treated as subagent spawns instead of ACP harness sessions. In practice, ACP-specific behavior like ACP agent selection and cwd handling is lost, so codex/claude-code requests can run in the wrong runtime and fail user workflows that previously worked.

Useful? React with 👍 / 👎.

Comment on lines +56 to +58
if (groupIds && session.sessionId) {
const agentPart = session.sessionId.split(":")[1];
if (agentPart && !groupIds.has(agentPart)) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Match quota groupIds against real agent IDs

groupIds filtering currently parses session.sessionId with split(":")[1], but session IDs are ordinary IDs (no embedded agent segment), so agentPart is typically undefined and the filter does not exclude non-matching sessions. That makes scoped quota checks aggregate the wrong data, which can incorrectly allow or block budget-gated plugin behavior for group-based policies.

Useful? React with 👍 / 👎.

@dgarson dgarson changed the base branch from main to dgarson/fork March 3, 2026 01:02
dgarson added a commit that referenced this pull request Mar 3, 2026
Reconstructed from PR #190 onto clean ancestry; merged by Xavier during worker-reactivation cycle.
@dgarson
Copy link
Owner Author

dgarson commented Mar 3, 2026

Conflict resolution update: merged origin/dgarson/fork into this PR branch and resolved all merge conflicts, then pushed commit a4257f6783ccaade4ce1fd6492786784d885940b.

Current PR state:

  • mergeable: MERGEABLE
  • mergeStateStatus: UNSTABLE (remaining CI/check gates)

Targeted test attempt:

  • Tried running pnpm -s vitest run src/telegram/bot.create-telegram-bot.test.ts src/slack/send.blocks.test.ts src/web/media.test.ts
  • Blocked at startup by workspace-level config parse failure in ../package.json (outside this branch context in current local environment), so no test execution completed.

Next gates:

  1. Re-run CI on PR after this merge commit.
  2. Verify required checks pass and resolve any non-conflict failures if they appear.

@dgarson
Copy link
Owner Author

dgarson commented Mar 3, 2026

CI triage update (post-conflict-resolution):\n\n- Current PR head: a4257f6783ccaade4ce1fd6492786784d885940b\n- Required checks are not failing; they are all stuck in queued state:\n - CI / docs-scope\n - CI / secrets\n - Install Smoke / docs-scope\n - Labeler / label\n - Labeler / label-issues\n- This appears to be infra-side runner backlog/outage (repo-wide runs are queued, including unrelated branches).\n\nI can’t push this PR to green until runners start executing jobs.\nOnce queue clears, I’ll re-check immediately and handle any real failures.

@dgarson
Copy link
Owner Author

dgarson commented Mar 3, 2026

CI status check (2026-03-03 05:50 MST):\n\n- Not merge-ready yet. Required checks are still queued.\n- Current blocker: GitHub Actions jobs have not started execution (still in QUEUED state), so no passing signal is available yet.\n\nQueued checks observed:\n- CI / docs-scope\n- CI / secrets\n- Install Smoke / docs-scope\n- Labeler / label\n- Labeler / label-issues\n\nSkipped (non-blocking) checks:\n- CI / ios\n- Labeler / backfill-pr-labels\n\nI’ll mark merge-ready as soon as required checks turn green.

@dgarson
Copy link
Owner Author

dgarson commented Mar 3, 2026

CI status check (2026-03-03 12:06 MST): still queued. Required checks are pending with 0s runtime (docs-scope, secrets, label, label-issues); no CI job has started executing yet. Current blocker still appears to be runner starvation on the blacksmith-16vcpu-ubuntu-2404 pool. Not merge-ready yet — I’ll mark merge-ready as soon as required checks go green.

Runs: https://github.com/dgarson/clawdbot/actions/runs/22623352014 and https://github.com/dgarson/clawdbot/actions/runs/22623351470

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.