Skip to content

Commit a894a55

Browse files
author
Rios Bot
committed
feat(cron): surface fallback progress
1 parent 1a3ce7c commit a894a55

27 files changed

Lines changed: 1011 additions & 22 deletions
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
---
2+
summary: "Lightweight production AI governance checklist for OpenClaw architecture changes"
3+
read_when:
4+
- Adopting external AI architecture advice for OpenClaw
5+
- Adding evaluation, replay, tracing, cost, or agent context governance
6+
- Reviewing whether an AI system change needs a larger architecture project
7+
title: "Production AI governance"
8+
---
9+
10+
OpenClaw already has several production AI building blocks: scoped
11+
`AGENTS.md` files, workspace skills, memory search, provider replay policies,
12+
diagnostic traces, session usage accounting, plugin lifecycle traces, channel
13+
routing, and QA lanes. Use this page when an external production AI checklist
14+
or architecture diagram looks useful, but the right answer is governance and
15+
verification rather than a repo-wide rewrite.
16+
17+
The default stance is lightweight adoption:
18+
19+
- Strengthen existing OpenClaw contracts before adding new layers.
20+
- Prefer evidence, replay, and targeted docs over broad directory reshuffles.
21+
- Keep core extension-agnostic; owner-specific behavior stays in the owning
22+
plugin, channel, or provider.
23+
- Do not change live config, credentials, provider defaults, channel defaults,
24+
or runtime services as part of this pass.
25+
26+
## Adoption matrix
27+
28+
| External layer | OpenClaw mapping | Decision |
29+
| ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
30+
| App entry and config | Gateway, CLI, Control UI, typed config docs | Adapt through existing docs and config contracts |
31+
| Retrieval and memory | [Memory search](/concepts/memory-search), memory backends, QMD/session search | Adopt source and scope requirements; do not add a new retrieval stack by default |
32+
| Services and routing | [Agent loop](/concepts/agent-loop), [Channel routing](/channels/channel-routing), provider/runtime helpers | Adapt through existing owner boundaries |
33+
| Prompt and agent context | [System prompt](/concepts/system-prompt), scoped `AGENTS.md`, [Skills](/tools/skills), workspace files | Adopt as task contracts and scoped context, not hardcoded prompts |
34+
| Agents and tools | Agent runtime hooks, plugin hooks, tools, subagents | Adapt with documented hook points; avoid hidden contract bypasses |
35+
| Security guards | Gateway security, tool policy, exec approvals, sandboxing, hooks | Adapt; fix the core path first unless the surface is high risk |
36+
| Evaluation | Unit tests, replay tests, QA lanes, live tests | Adopt a minimum golden and replay contract for high-risk paths |
37+
| Observability and cost | Diagnostics, stability events, traces, session usage, `/usage`, `/status` | Adopt a stage-level checklist before adding new telemetry backends |
38+
| Data boundaries | Memory roots, session stores, external artifacts such as cron task ledgers and command lane snapshots | Adapt via explicit snapshot or artifact boundaries |
39+
| Docs and coding agent context | Docs read hints, scoped `AGENTS.md`, skills, repo rules | Adopt; keep guidance discoverable and scoped |
40+
| Semantic cache, query rewriter, adaptive router | No default adoption | Defer until cost or quality evidence proves the need |
41+
| Repo-wide folder structure | Existing OpenClaw layout and owner boundaries | Reject as-is |
42+
43+
## Minimum evaluation contract
44+
45+
Any high-risk AI pipeline should have at least one executable or documented
46+
golden case for each relevant behavior below. Link to existing tests when they
47+
already cover the behavior; do not invent a parallel test harness only to match
48+
an external checklist.
49+
50+
- **Memory retrieval:** results preserve source, scope, and degraded-mode
51+
behavior. A memory failure should not silently become unscoped context.
52+
- **Provider and tool replay:** replay preserves transcript bytes and tool
53+
results unless a documented repair path intentionally rewrites them.
54+
- **Channel routing:** every configured inbound surface is named separately.
55+
DMs, groups, channels, threads, mentions, slash commands, webhooks, and
56+
native command delivery are distinct surfaces when the config exposes them.
57+
- **Agent context:** scoped `AGENTS.md`, workspace files, skills, and memory
58+
rules are loaded through the documented prompt/context path.
59+
- **Cost and context growth:** unusually large token, context, or cache-read
60+
changes can be traced back to a session, model, provider, or stage.
61+
62+
Use [Testing](/help/testing) for regular suite selection and QA commands.
63+
64+
## Observability checklist
65+
66+
Before a high-risk pipeline change is considered production-ready, an operator
67+
should be able to answer these questions from existing logs, diagnostics,
68+
session metadata, or test artifacts:
69+
70+
- Which stage failed or produced the surprising output?
71+
- Which session, agent, model, provider, channel, and inbound surface were
72+
involved?
73+
- What input and output summaries are safe to inspect without exposing raw
74+
payloads or credentials?
75+
- Are token, cache, and cost changes attributable to a stage or provider?
76+
- Can user feedback, a support report, or a diagnostics bundle be linked back
77+
to the relevant run?
78+
- Is the failure mode covered by a targeted test, replay, QA scenario, or live
79+
smoke?
80+
81+
Start with existing surfaces such as diagnostics export, stability events,
82+
session usage, cache traces, plugin lifecycle traces, and provider replay tests.
83+
Add a new telemetry backend only after this checklist shows an actual gap.
84+
85+
## Agent context boundaries
86+
87+
The coding-agent context layer maps to OpenClaw's existing scoped context
88+
system:
89+
90+
- Repo rules live in `AGENTS.md` and scoped `AGENTS.md` files.
91+
- Agent persona and workspace memory live in the agent workspace, not in repo
92+
docs.
93+
- Skills provide reusable operating procedures and should stay scoped to the
94+
agent or plugin that needs them.
95+
- System prompt changes must go through the documented prompt assembly path.
96+
- Multi-agent or multi-persona setups need separate workspaces and agent state;
97+
do not rely on `agentId` alone to isolate persona, memory, or auth behavior.
98+
99+
When a new channel, plugin, agent, or automation surface is added, document the
100+
inbound surfaces it exposes and the context sources it is allowed to load.
101+
102+
## Stop rules
103+
104+
Stop this lightweight adoption path and open a separate architecture project if
105+
the change requires any of the following:
106+
107+
- Public SDK or Gateway protocol changes.
108+
- Provider defaults, channel defaults, credential handling, or live config
109+
mutation.
110+
- A new telemetry backend, database, queue, or external service.
111+
- Moving owner-specific extension behavior into core without evidence that
112+
multiple owners need a generic seam.
113+
- Semantic cache, query rewriting, or adaptive routing without concrete cost,
114+
quality, or reliability evidence.
115+
- Repo-wide folder or package restructuring.
116+
117+
## Verification entry points
118+
119+
For docs-only governance changes, use:
120+
121+
```bash
122+
pnpm docs:list
123+
git diff --check
124+
```
125+
126+
If trace, cost, replay, routing, or runtime code changes are made, run the
127+
smallest targeted test that covers the touched surface first. Escalate to
128+
`pnpm check:changed` when shared runtime, config, protocol, or public contract
129+
behavior changes.
130+
131+
## Related
132+
133+
- [Gateway architecture](/concepts/architecture)
134+
- [Agent loop](/concepts/agent-loop)
135+
- [System prompt](/concepts/system-prompt)
136+
- [Agent workspace](/concepts/agent-workspace)
137+
- [Memory search](/concepts/memory-search)
138+
- [Channel routing](/channels/channel-routing)
139+
- [Usage tracking](/concepts/usage-tracking)
140+
- [Diagnostics export](/gateway/diagnostics)
141+
- [Testing](/help/testing)

docs/concepts/architecture.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ Details: [Gateway protocol](/gateway/protocol), [Pairing](/channels/pairing),
148148

149149
## Related
150150

151+
- [Production AI governance](/architecture/production-ai-governance) — lightweight adoption checklist for evaluation, replay, tracing, cost, and agent context governance
151152
- [Agent Loop](/concepts/agent-loop) — detailed agent execution cycle
152153
- [Gateway Protocol](/gateway/protocol) — WebSocket protocol contract
153154
- [Queue](/concepts/queue) — command queue and concurrency

docs/concepts/model-failover.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,9 @@ The persisted fallback override closes that window, and the narrow rollback keep
381381
- optional status/code
382382
- human-readable error summary
383383

384-
Structured `model_fallback_decision` logs also include flat `fallbackStep*` fields when a candidate fails, is skipped, or a later fallback succeeds. These fields make the attempted transition explicit (`fallbackStepFromModel`, `fallbackStepToModel`, `fallbackStepFromFailureReason`, `fallbackStepFromFailureDetail`, `fallbackStepFinalOutcome`) so log and diagnostic exporters can reconstruct the primary failure even when the terminal fallback also fails.
384+
Structured `model_fallback_decision` logs also include flat `fallbackStep*` fields when a candidate fails, is skipped, or a later fallback succeeds. These fields make the attempted transition explicit (`fallbackStepFromModel`, `fallbackStepToModel`, `fallbackStepFromFailureReason`, `fallbackStepFromFailureDetail`, `fallbackStepFinalOutcome`) and include aggregate queue pressure (`fallbackStepQueueActive`, `fallbackStepQueueQueued`, `fallbackStepQueueDraining`) so log and diagnostic exporters can reconstruct the primary failure even when the terminal fallback also fails.
385+
386+
Isolated cron agent runs mirror the same primary/fallback/queue chain into the cron task `progressSummary`, so `openclaw tasks show <task>` can identify the primary model, fallback target, failure reason, and queue/drain state without replaying gateway logs.
385387

386388
When every candidate fails, OpenClaw throws `FallbackSummaryError`. The outer reply runner can use that to build a more specific message such as "all models are temporarily rate-limited" and include the soonest cooldown expiry when one is known.
387389

docs/docs.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1151,6 +1151,7 @@
11511151
"group": "Fundamentals",
11521152
"pages": [
11531153
"concepts/architecture",
1154+
"architecture/production-ai-governance",
11541155
"concepts/agent",
11551156
"concepts/agent-loop",
11561157
"concepts/agent-runtimes",

docs/help/testing.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,26 @@ of Docker runners. This doc is a "how we test" guide:
2525
This page covers running the regular test suites and Docker/Parallels runners. The QA-specific runners section below ([QA-specific runners](#qa-specific-runners)) lists the concrete `qa` invocations and points back at the references above.
2626
</Note>
2727

28+
## Production AI regression contract
29+
30+
For high-risk AI pipeline changes, pair the regular test selection below with
31+
the lightweight [Production AI governance](/architecture/production-ai-governance)
32+
contract. The minimum golden coverage is:
33+
34+
- memory retrieval preserves source, scope, and degraded-mode behavior
35+
- provider and tool replay preserves transcript bytes unless a documented
36+
repair path intentionally rewrites them
37+
- channel routing names every configured inbound surface separately, including
38+
DMs, groups, channels, threads, mentions, slash commands, webhooks, and native
39+
command delivery when they exist
40+
- agent context enters through scoped `AGENTS.md`, workspace files, skills, and
41+
the documented system prompt path
42+
- token, cache, and cost changes can be attributed to a session, provider, model,
43+
or pipeline stage
44+
45+
When existing tests already cover a behavior, link to or run those tests instead
46+
of adding a parallel harness only to satisfy a checklist.
47+
2848
## Quick start
2949

3050
Most days:
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
#!/usr/bin/env tsx
2+
import { mkdtempSync } from "node:fs";
3+
import { tmpdir } from "node:os";
4+
import path from "node:path";
5+
6+
const stateDir = mkdtempSync(path.join(tmpdir(), "openclaw-pr-87343-proof-"));
7+
process.env.OPENCLAW_STATE_DIR = stateDir;
8+
9+
const {
10+
GatewayDrainingError,
11+
enqueueCommand,
12+
enqueueCommandInLane,
13+
getCommandLaneSnapshot,
14+
markGatewayDraining,
15+
resetCommandQueueStateForTest,
16+
} = await import("../../src/process/command-queue.js");
17+
const {
18+
createRunningTaskRun,
19+
recordTaskRunProgressByRunId,
20+
resetDetachedTaskLifecycleRuntimeForTests,
21+
} = await import("../../src/tasks/detached-task-runtime.js");
22+
const { findTaskByRunId, resetTaskRegistryForTests } =
23+
await import("../../src/tasks/task-registry.js");
24+
const { resetTaskFlowRegistryForTests } = await import("../../src/tasks/task-flow-registry.js");
25+
const { FailoverError } = await import("../../src/agents/failover-error.js");
26+
const { runWithModelFallback } = await import("../../src/agents/model-fallback.js");
27+
28+
const runId = "pr-87343-cron-proof";
29+
const sessionLane = "session:pr-87343-cron-proof";
30+
const globalLane = "agent:cron:pr-87343-cron-proof";
31+
const primary = "openai/gpt-4.1-mini";
32+
const fallback = "anthropic/claude-haiku-3-5";
33+
const cfg = {
34+
agents: {
35+
defaults: {
36+
model: {
37+
primary,
38+
fallbacks: [fallback],
39+
},
40+
},
41+
},
42+
};
43+
44+
function formatProgressSummary(step?: {
45+
fallbackStepFromModel: string;
46+
fallbackStepToModel?: string;
47+
fallbackStepFromFailureReason?: string;
48+
fallbackStepFinalOutcome: string;
49+
fallbackStepFromFailureDetail?: string;
50+
fallbackStepQueueActive?: number;
51+
fallbackStepQueueQueued?: number;
52+
fallbackStepQueueDraining?: boolean;
53+
}): string {
54+
if (!step) {
55+
return `model primary=${primary}; queue active=0 queued=0 draining=no`;
56+
}
57+
const target = step.fallbackStepToModel ? ` -> ${step.fallbackStepToModel}` : "";
58+
const reason = step.fallbackStepFromFailureReason
59+
? ` reason=${step.fallbackStepFromFailureReason}`
60+
: "";
61+
const detail = step.fallbackStepFromFailureDetail
62+
? ` detail=${step.fallbackStepFromFailureDetail}`
63+
: "";
64+
const active = step.fallbackStepQueueActive ?? 0;
65+
const queued = step.fallbackStepQueueQueued ?? 0;
66+
const draining = step.fallbackStepQueueDraining === true ? "yes" : "no";
67+
return `model fallback: ${step.fallbackStepFromModel}${target}${reason} outcome=${step.fallbackStepFinalOutcome}${detail}; queue active=${active} queued=${queued} draining=${draining}`;
68+
}
69+
70+
resetDetachedTaskLifecycleRuntimeForTests();
71+
resetTaskRegistryForTests({ persist: false });
72+
resetTaskFlowRegistryForTests({ persist: false });
73+
resetCommandQueueStateForTest();
74+
75+
const task = createRunningTaskRun({
76+
runtime: "cron",
77+
ownerKey: "agent:main:main",
78+
scopeKind: "session",
79+
runId,
80+
task: "PR #87343 cron fallback progress proof",
81+
startedAt: Date.now(),
82+
progressSummary: formatProgressSummary(),
83+
});
84+
85+
if (!task) {
86+
throw new Error("failed to create cron proof task run");
87+
}
88+
89+
console.log(`[proof] stateDir=${stateDir}`);
90+
console.log(`[proof] task created runtime=${task.runtime} runId=${task.runId}`);
91+
console.log(`[proof] initial progressSummary=${task.progressSummary}`);
92+
93+
const activeTask = enqueueCommand(async () => {
94+
console.log(`[proof] active queue before fallback=${JSON.stringify(getCommandLaneSnapshot())}`);
95+
let embeddedFallbackContinuationObserved = false;
96+
const result = await runWithModelFallback({
97+
cfg,
98+
provider: "openai",
99+
model: "gpt-4.1-mini",
100+
manifestPlugins: [],
101+
skipAuthProfileRuntime: true,
102+
allowGatewayDrainingContinuation: true,
103+
onFallbackStep: (step) => {
104+
const progressSummary = formatProgressSummary(step);
105+
const updated = recordTaskRunProgressByRunId({
106+
runId,
107+
runtime: "cron",
108+
lastEventAt: Date.now(),
109+
progressSummary,
110+
});
111+
console.log(`[proof] fallback step=${JSON.stringify(step)}`);
112+
console.log(`[proof] recorded progressSummary=${updated.at(-1)?.progressSummary}`);
113+
},
114+
run: async (provider, model, options) => {
115+
console.log(
116+
`[proof] attempt provider=${provider} model=${model} allowGatewayDrainingContinuation=${options?.allowGatewayDrainingContinuation === true}`,
117+
);
118+
return enqueueCommandInLane(
119+
sessionLane,
120+
() =>
121+
enqueueCommandInLane(
122+
globalLane,
123+
async () => {
124+
if (provider === "openai") {
125+
markGatewayDraining();
126+
console.log("[proof] gateway drain marked while embedded queue work is active");
127+
throw new FailoverError("primary rate limited", {
128+
reason: "rate_limit",
129+
provider,
130+
model,
131+
status: 429,
132+
code: "RESOURCE_EXHAUSTED",
133+
});
134+
}
135+
embeddedFallbackContinuationObserved =
136+
options?.allowGatewayDrainingContinuation === true;
137+
return "fallback ok";
138+
},
139+
{
140+
allowGatewayDrainingContinuation: options?.allowGatewayDrainingContinuation === true,
141+
},
142+
),
143+
{
144+
allowGatewayDrainingContinuation: options?.allowGatewayDrainingContinuation === true,
145+
},
146+
);
147+
},
148+
});
149+
if (!embeddedFallbackContinuationObserved) {
150+
throw new Error("fallback attempt did not run as an embedded queue continuation");
151+
}
152+
console.log(`[proof] active task result=${result.provider}/${result.model}:${result.result}`);
153+
return result;
154+
});
155+
156+
await activeTask;
157+
158+
try {
159+
await enqueueCommand(async () => "should not enqueue during drain");
160+
throw new Error("enqueue unexpectedly succeeded during gateway drain");
161+
} catch (error) {
162+
if (!(error instanceof GatewayDrainingError)) {
163+
throw error;
164+
}
165+
console.log("[proof] new enqueue rejected with GatewayDrainingError");
166+
}
167+
168+
const finalTask = findTaskByRunId(runId);
169+
console.log(`[proof] final task progressSummary=${finalTask?.progressSummary}`);
170+
console.log(
171+
"[proof] PASS embedded queue-shaped fallback continued during drain; new enqueue still rejected",
172+
);

src/agents/agent-command.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1728,6 +1728,7 @@ async function agentCommandInternal(
17281728
model: modelLocal,
17291729
result: resultLocal,
17301730
}),
1731+
allowGatewayDrainingContinuation: true,
17311732
abortSignal: opts.abortSignal,
17321733
run: async (providerOverride, modelOverride, runOptions) => {
17331734
const isAutoFallbackPrimaryProbeCandidate =
@@ -1790,6 +1791,8 @@ async function agentCommandInternal(
17901791
pluginsEnabled,
17911792
...(manifestMetadataSnapshot ? { metadataSnapshot: manifestMetadataSnapshot } : {}),
17921793
allowTransientCooldownProbe: runOptions?.allowTransientCooldownProbe,
1794+
allowGatewayDrainingContinuation:
1795+
runOptions?.allowGatewayDrainingContinuation === true,
17931796
sessionHasHistory:
17941797
!isNewSession ||
17951798
(await attemptExecutionRuntime.sessionFileHasContent(attemptSessionFile)),

0 commit comments

Comments
 (0)