feat: real user flow validation scripts + heartbeat timing fixes - #282

Closed
bussyjd wants to merge 1 commit into
mainfrom
feat/autoresearch-flows
Closed

feat: real user flow validation scripts + heartbeat timing fixes#282
bussyjd wants to merge 1 commit into
mainfrom
feat/autoresearch-flows

Conversation

@bussyjd

Copy link
Copy Markdown
Contributor

Summary

Introduces pi-autoresearch integration for automated real user flow validation, plus Go bug fixes and doc corrections discovered during the overnight run.

Commit 1: Flow scripts + autoresearch harness

10 bash flow scripts that exercise every documented user journey from docs/getting-started.md and docs/guides/monetize-inference.md:

flows/
├── lib.sh # Shared harness (step/pass/fail/poll, Anvil credentials)
├── flow-01-prerequisites.sh # Docker, Ollama, obol binary
├── flow-02-stack-init-up.sh # getting-started §1-2
├── flow-03-inference.sh # getting-started §3a-3d (LiteLLM, tool-calls)
├── flow-04-agent.sh # getting-started §4-5 (agent init, inference)
├── flow-05-network.sh # getting-started §6 (network add/remove)
├── flow-06-sell-setup.sh # monetize §1.1-1.4 (pricing, ServiceOffer, heartbeat)
├── flow-07-sell-verify.sh # monetize §1.5-1.7 (tunnel, routes, 402, metrics)
├── flow-08-buy.sh # monetize §2 (discovery, EIP-712 payment, settlement)
├── flow-09-lifecycle.sh # monetize §4 (stop, delete, cleanup)
└── flow-10-anvil-facilitator.sh # monetize §3 (Anvil fork, x402-rs facilitator)

All commands use the real obol binary — no go run, no direct kubectl, no pod IP access.

Commit 2: Bug fixes found by the flow scripts

Timing (closes#280):

  • ensureHeartbeatActive() — heartbeat was stuck at 30m default after obol agent init
  • patchHeartbeatAfterSync()SyncAgentBaseURL helmfile sync was resetting heartbeat config
  • readCurrentAgentBaseURL() — skip sync when tunnel URL unchanged (idempotency)

CLI validation:

  • validateRPCEndpoint() in obol network add — reject invalid URLs

Doc corrections:

  • getting-started §3c-3d: add LiteLLM Bearer auth header (was returning 401)
  • monetize §1.6: correct eRPC path to /rpc/evm/{chainId} (local-only, not via tunnel)
  • monetize §2.1: clarify /skill.md (always available) vs /.well-known (requires --register)

Discovery method

Overnight pi-autoresearch session: 44 → 133 steps passing across 50+ experiments. The agent autonomously ran flow scripts, diagnosed failures, fixed Go code and flow scripts, and re-validated — all while go build/go test acted as backpressure.

Test plan

  • go build ./... passes
  • go test ./internal/{agent,tunnel,network,openclaw,model}/ passes
  • pi-autoresearch: 133/133 steps, 50+ runs
  • Manual: bash flows/flow-01-prerequisites.sh through flow-09-lifecycle.sh on fresh cluster

@bussyjd
bussyjdforce-pushed the feat/autoresearch-flows branch from c040301 to e9683b1CompareMarch 19, 2026 10:03
Introduces pi-autoresearch integration with 10 flow scripts that exercise
every documented user journey from getting-started.md and monetize-inference.md.
Flow scripts (flows/):
- flow-01: prerequisites (Docker, Ollama, obol binary)
- flow-02: stack init + up + verify (getting-started §1-2)
- flow-03: LLM inference chain (getting-started §3a-3d)
- flow-04: agent init + inference (getting-started §4-5)
- flow-05: network add/remove (getting-started §6)
- flow-06: sell setup — pricing, ServiceOffer, heartbeat wait (monetize §1.1-1.4)
- flow-07: sell verify — tunnel, routes, 402, metrics (monetize §1.5-1.7)
- flow-08: buy — discovery, 402 parse, EIP-712 payment, settlement (monetize §2)
- flow-09: lifecycle — stop, delete, cleanup verification (monetize §4)
- flow-10: Anvil fork + x402-rs facilitator setup (monetize §3)
Bug fixes found by the flow scripts:
- ensureHeartbeatActive() — heartbeat stuck at 30m default after obol agent init
- patchHeartbeatAfterSync() — SyncAgentBaseURL helmfile sync was resetting heartbeat
- validateRPCEndpoint() — obol network add was accepting invalid URLs
- Doc corrections: LiteLLM auth, eRPC path, /skill.md vs /.well-known
Test account keys are derived at runtime from the Anvil mnemonic via
`cast wallet private-key` — no private keys in source.
Closes#280
@bussyjd
bussyjdforce-pushed the feat/autoresearch-flows branch from e9683b1 to e56ae6dCompareMarch 19, 2026 10:09
@bussyjd
bussyjd requested a review from OisinKyneMarch 19, 2026 10:20
@bussyjdbussyjd changed the title feat: autoresearch flow scripts + heartbeat/CLI/doc fixesfeat: real user flow validation scripts + heartbeat timing fixesMar 19, 2026

@OisinKyneOisinKyne left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

i'm a little unsure of the accuracy of the flows, at least they imply features i dont expect. I also think we probably need the heartbeat to be agent writable. I tried to get my obolclaw to modify key facts about itself and it was unable to (tuning down a 2 minute heartbeat was one). I don't know if giving it the auth to write to a config map is worth it, if instead we can get these files all mounted into the openclaw pv, and then obol cli just messes with the files on the host (e.g. how obol model sync can change the openclaw agent definition).

To that end, i might pull some of the fixes in separately to this pr, so as not to delay waiting for feedback. (the fact that the flow tests aren't in the user hot path makes me less worried about them being imperfect)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm a little confused by some of the changes in this file (but its a docs so nbd

Comment on lines +44 to +45
# Use qwen3.5:9b — it is configured in LiteLLM's model_list (FLOW_MODEL qwen3:0.6b
# is only registered in Ollama directly; the x402 sell/buy flows use it via that path)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

qwen3:0.6b, should we have a qwen3.5:?

out=$("$OBOL" kubectl exec -n llm deployment/litellm -c litellm -- \
python3 -c "
import urllib.request
r = urllib.request.urlopen('http://ollama.llm.svc.cluster.local:11434/api/tags', timeout=10)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we have internal ollama code? maybe we should internalise a llama.cpp pod

fail "LiteLLM inference failed — ${out:0:300}"
fi

# §3d: Tool-call passthrough

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Tool call at the litellm layer seems like it might confuse things

Comment on lines +147 to +227
func ensureHeartbeatActive(cfg *config.Config, u *ui.UI) error {
namespace := fmt.Sprintf("openclaw-%s", DefaultInstanceID)
kubectlBin := filepath.Join(cfg.BinDir, "kubectl")
kubeconfigPath := filepath.Join(cfg.ConfigDir, "kubeconfig.yaml")
env := append(os.Environ(), fmt.Sprintf("KUBECONFIG=%s", kubeconfigPath))

// Read current ConfigMap.
getCmd := exec.Command(kubectlBin,
"get", "configmap", "openclaw-config",
"-n", namespace,
"-o", "jsonpath={.data.openclaw\\.json}")
getCmd.Env = env
var outBuf bytes.Buffer
getCmd.Stdout = &outBuf
if err := getCmd.Run(); err != nil {
return fmt.Errorf("read openclaw-config: %w", err)
}

var cfgJSON map[string]interface{}
if err := json.Unmarshal(outBuf.Bytes(), &cfgJSON); err != nil {
return fmt.Errorf("parse openclaw.json: %w", err)
}

// Check whether heartbeat is already present.
agents, _ := cfgJSON["agents"].(map[string]interface{})
defaults, _ := agents["defaults"].(map[string]interface{})
_, alreadySet := defaults["heartbeat"]
if alreadySet {
u.Success("Heartbeat config already active")
return nil
}

// Inject heartbeat.
if agents == nil {
agents = map[string]interface{}{}
cfgJSON["agents"] = agents
}
if defaults == nil {
defaults = map[string]interface{}{}
agents["defaults"] = defaults
}
defaults["heartbeat"] = map[string]interface{}{
"every": "5m",
"target": "none",
}

patched, err := json.MarshalIndent(cfgJSON, "", " ")
if err != nil {
return fmt.Errorf("marshal patched config: %w", err)
}

applyPayload := map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "openclaw-config",
"namespace": namespace,
},
"data": map[string]string{
"openclaw.json": string(patched),
},
}
applyRaw, _ := json.Marshal(applyPayload)

applyCmd := exec.Command(kubectlBin,
"apply", "-f", "-",
"--server-side", "--field-manager=helm", "--force-conflicts")
applyCmd.Env = env
applyCmd.Stdin = bytes.NewReader(applyRaw)
var applyErr bytes.Buffer
applyCmd.Stderr = &applyErr
if err := applyCmd.Run(); err != nil {
return fmt.Errorf("patch heartbeat config: %w\n%s", err, applyErr.String())
}

// OpenClaw watches for ConfigMap file changes and hot-reloads config.
// No pod restart is needed: the running pod will detect the update within
// ~30-60s and apply [reload] config hot reload, switching the heartbeat
// interval to 5m immediately without losing the running pod or its state.
u.Success("Heartbeat config injected — OpenClaw hot reload will activate it (every 5m)")
return nil

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Mounting openclaw stuff like this as a config map means its read only to the runtime. This has ux problems because openclaw can't tune stuff for itself. If instead we do this with files on the host maybe they'll be mutable by the openclaw service?

Comment on lines +1 to +3
{
"workingDir": "/Users/bussyjd/Development/Obol_Workbench/obol-stack/.worktrees/autoresearch"
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

remove

Comment threadautoresearch.md
Comment on lines +174 to +177
### Session 1 (baseline → 61/61)

**Baseline: 44/57** — 13 failures across all flows.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should we move this to ./flows or somewhere other than top level if its not for users?

Comment threadautoresearch.sh
@@ -0,0 +1,48 @@
#!/bin/bash

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

subdirectory

@bussyjd

Copy link
Copy Markdown
ContributorAuthor

Closing — core CLI/Go changes were cherry-picked into feat/monetize-path (PR #288). The autoresearch shell scripts and top-level files flagged in @OisinKyne's review (hardcoded paths in autoresearch.config.json, autoresearch.sh/.md at root) were intentionally not carried over. Review feedback addressed by omission.

@OisinKyne
OisinKyne deleted the feat/autoresearch-flows branch April 23, 2026 19:26
Sign up for freeto 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.

fix: heartbeat timing bugs — reset on sell, missing activation, ConfigMap race

2 participants

@bussyjd@OisinKyne
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat: real user flow validation scripts + heartbeat timing fixes - #282

Closed
bussyjd wants to merge 1 commit into
mainfrom
feat/autoresearch-flows
Closed

feat: real user flow validation scripts + heartbeat timing fixes#282
bussyjd wants to merge 1 commit into
mainfrom
feat/autoresearch-flows

Conversation

@bussyjd

Copy link
Copy Markdown
Contributor

Summary

Introduces pi-autoresearch integration for automated real user flow validation, plus Go bug fixes and doc corrections discovered during the overnight run.

Commit 1: Flow scripts + autoresearch harness

10 bash flow scripts that exercise every documented user journey from docs/getting-started.md and docs/guides/monetize-inference.md:

flows/
├── lib.sh # Shared harness (step/pass/fail/poll, Anvil credentials)
├── flow-01-prerequisites.sh # Docker, Ollama, obol binary
├── flow-02-stack-init-up.sh # getting-started §1-2
├── flow-03-inference.sh # getting-started §3a-3d (LiteLLM, tool-calls)
├── flow-04-agent.sh # getting-started §4-5 (agent init, inference)
├── flow-05-network.sh # getting-started §6 (network add/remove)
├── flow-06-sell-setup.sh # monetize §1.1-1.4 (pricing, ServiceOffer, heartbeat)
├── flow-07-sell-verify.sh # monetize §1.5-1.7 (tunnel, routes, 402, metrics)
├── flow-08-buy.sh # monetize §2 (discovery, EIP-712 payment, settlement)
├── flow-09-lifecycle.sh # monetize §4 (stop, delete, cleanup)
└── flow-10-anvil-facilitator.sh # monetize §3 (Anvil fork, x402-rs facilitator)

All commands use the real obol binary — no go run, no direct kubectl, no pod IP access.

Commit 2: Bug fixes found by the flow scripts

Timing (closes#280):

  • ensureHeartbeatActive() — heartbeat was stuck at 30m default after obol agent init
  • patchHeartbeatAfterSync()SyncAgentBaseURL helmfile sync was resetting heartbeat config
  • readCurrentAgentBaseURL() — skip sync when tunnel URL unchanged (idempotency)

CLI validation:

  • validateRPCEndpoint() in obol network add — reject invalid URLs

Doc corrections:

  • getting-started §3c-3d: add LiteLLM Bearer auth header (was returning 401)
  • monetize §1.6: correct eRPC path to /rpc/evm/{chainId} (local-only, not via tunnel)
  • monetize §2.1: clarify /skill.md (always available) vs /.well-known (requires --register)

Discovery method

Overnight pi-autoresearch session: 44 → 133 steps passing across 50+ experiments. The agent autonomously ran flow scripts, diagnosed failures, fixed Go code and flow scripts, and re-validated — all while go build/go test acted as backpressure.

Test plan

  • go build ./... passes
  • go test ./internal/{agent,tunnel,network,openclaw,model}/ passes
  • pi-autoresearch: 133/133 steps, 50+ runs
  • Manual: bash flows/flow-01-prerequisites.sh through flow-09-lifecycle.sh on fresh cluster

@bussyjd
bussyjdforce-pushed the feat/autoresearch-flows branch from c040301 to e9683b1CompareMarch 19, 2026 10:03
Introduces pi-autoresearch integration with 10 flow scripts that exercise
every documented user journey from getting-started.md and monetize-inference.md.
Flow scripts (flows/):
- flow-01: prerequisites (Docker, Ollama, obol binary)
- flow-02: stack init + up + verify (getting-started §1-2)
- flow-03: LLM inference chain (getting-started §3a-3d)
- flow-04: agent init + inference (getting-started §4-5)
- flow-05: network add/remove (getting-started §6)
- flow-06: sell setup — pricing, ServiceOffer, heartbeat wait (monetize §1.1-1.4)
- flow-07: sell verify — tunnel, routes, 402, metrics (monetize §1.5-1.7)
- flow-08: buy — discovery, 402 parse, EIP-712 payment, settlement (monetize §2)
- flow-09: lifecycle — stop, delete, cleanup verification (monetize §4)
- flow-10: Anvil fork + x402-rs facilitator setup (monetize §3)
Bug fixes found by the flow scripts:
- ensureHeartbeatActive() — heartbeat stuck at 30m default after obol agent init
- patchHeartbeatAfterSync() — SyncAgentBaseURL helmfile sync was resetting heartbeat
- validateRPCEndpoint() — obol network add was accepting invalid URLs
- Doc corrections: LiteLLM auth, eRPC path, /skill.md vs /.well-known
Test account keys are derived at runtime from the Anvil mnemonic via
`cast wallet private-key` — no private keys in source.
Closes#280
@bussyjd
bussyjdforce-pushed the feat/autoresearch-flows branch from e9683b1 to e56ae6dCompareMarch 19, 2026 10:09
@bussyjd
bussyjd requested a review from OisinKyneMarch 19, 2026 10:20
@bussyjdbussyjd changed the title feat: autoresearch flow scripts + heartbeat/CLI/doc fixesfeat: real user flow validation scripts + heartbeat timing fixesMar 19, 2026

@OisinKyneOisinKyne left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

i'm a little unsure of the accuracy of the flows, at least they imply features i dont expect. I also think we probably need the heartbeat to be agent writable. I tried to get my obolclaw to modify key facts about itself and it was unable to (tuning down a 2 minute heartbeat was one). I don't know if giving it the auth to write to a config map is worth it, if instead we can get these files all mounted into the openclaw pv, and then obol cli just messes with the files on the host (e.g. how obol model sync can change the openclaw agent definition).

To that end, i might pull some of the fixes in separately to this pr, so as not to delay waiting for feedback. (the fact that the flow tests aren't in the user hot path makes me less worried about them being imperfect)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm a little confused by some of the changes in this file (but its a docs so nbd

Comment on lines +44 to +45
# Use qwen3.5:9b — it is configured in LiteLLM's model_list (FLOW_MODEL qwen3:0.6b
# is only registered in Ollama directly; the x402 sell/buy flows use it via that path)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

qwen3:0.6b, should we have a qwen3.5:?

out=$("$OBOL" kubectl exec -n llm deployment/litellm -c litellm -- \
python3 -c "
import urllib.request
r = urllib.request.urlopen('http://ollama.llm.svc.cluster.local:11434/api/tags', timeout=10)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we have internal ollama code? maybe we should internalise a llama.cpp pod

fail "LiteLLM inference failed — ${out:0:300}"
fi

# §3d: Tool-call passthrough

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Tool call at the litellm layer seems like it might confuse things

Comment on lines +147 to +227
func ensureHeartbeatActive(cfg *config.Config, u *ui.UI) error {
namespace := fmt.Sprintf("openclaw-%s", DefaultInstanceID)
kubectlBin := filepath.Join(cfg.BinDir, "kubectl")
kubeconfigPath := filepath.Join(cfg.ConfigDir, "kubeconfig.yaml")
env := append(os.Environ(), fmt.Sprintf("KUBECONFIG=%s", kubeconfigPath))

// Read current ConfigMap.
getCmd := exec.Command(kubectlBin,
"get", "configmap", "openclaw-config",
"-n", namespace,
"-o", "jsonpath={.data.openclaw\\.json}")
getCmd.Env = env
var outBuf bytes.Buffer
getCmd.Stdout = &outBuf
if err := getCmd.Run(); err != nil {
return fmt.Errorf("read openclaw-config: %w", err)
}

var cfgJSON map[string]interface{}
if err := json.Unmarshal(outBuf.Bytes(), &cfgJSON); err != nil {
return fmt.Errorf("parse openclaw.json: %w", err)
}

// Check whether heartbeat is already present.
agents, _ := cfgJSON["agents"].(map[string]interface{})
defaults, _ := agents["defaults"].(map[string]interface{})
_, alreadySet := defaults["heartbeat"]
if alreadySet {
u.Success("Heartbeat config already active")
return nil
}

// Inject heartbeat.
if agents == nil {
agents = map[string]interface{}{}
cfgJSON["agents"] = agents
}
if defaults == nil {
defaults = map[string]interface{}{}
agents["defaults"] = defaults
}
defaults["heartbeat"] = map[string]interface{}{
"every": "5m",
"target": "none",
}

patched, err := json.MarshalIndent(cfgJSON, "", " ")
if err != nil {
return fmt.Errorf("marshal patched config: %w", err)
}

applyPayload := map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "openclaw-config",
"namespace": namespace,
},
"data": map[string]string{
"openclaw.json": string(patched),
},
}
applyRaw, _ := json.Marshal(applyPayload)

applyCmd := exec.Command(kubectlBin,
"apply", "-f", "-",
"--server-side", "--field-manager=helm", "--force-conflicts")
applyCmd.Env = env
applyCmd.Stdin = bytes.NewReader(applyRaw)
var applyErr bytes.Buffer
applyCmd.Stderr = &applyErr
if err := applyCmd.Run(); err != nil {
return fmt.Errorf("patch heartbeat config: %w\n%s", err, applyErr.String())
}

// OpenClaw watches for ConfigMap file changes and hot-reloads config.
// No pod restart is needed: the running pod will detect the update within
// ~30-60s and apply [reload] config hot reload, switching the heartbeat
// interval to 5m immediately without losing the running pod or its state.
u.Success("Heartbeat config injected — OpenClaw hot reload will activate it (every 5m)")
return nil

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Mounting openclaw stuff like this as a config map means its read only to the runtime. This has ux problems because openclaw can't tune stuff for itself. If instead we do this with files on the host maybe they'll be mutable by the openclaw service?

Comment on lines +1 to +3
{
"workingDir": "/Users/bussyjd/Development/Obol_Workbench/obol-stack/.worktrees/autoresearch"
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

remove

Comment threadautoresearch.md
Comment on lines +174 to +177
### Session 1 (baseline → 61/61)

**Baseline: 44/57** — 13 failures across all flows.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should we move this to ./flows or somewhere other than top level if its not for users?

Comment threadautoresearch.sh
@@ -0,0 +1,48 @@
#!/bin/bash

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

subdirectory

@bussyjd

Copy link
Copy Markdown
ContributorAuthor

Closing — core CLI/Go changes were cherry-picked into feat/monetize-path (PR #288). The autoresearch shell scripts and top-level files flagged in @OisinKyne's review (hardcoded paths in autoresearch.config.json, autoresearch.sh/.md at root) were intentionally not carried over. Review feedback addressed by omission.

@OisinKyne
OisinKyne deleted the feat/autoresearch-flows branch April 23, 2026 19:26
Sign up for freeto 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.

fix: heartbeat timing bugs — reset on sell, missing activation, ConfigMap race

2 participants

@bussyjd@OisinKyne
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: real user flow validation scripts + heartbeat timing fixes - #282

Closed
bussyjd wants to merge 1 commit into
mainfrom
feat/autoresearch-flows
Closed

feat: real user flow validation scripts + heartbeat timing fixes#282
bussyjd wants to merge 1 commit into
mainfrom
feat/autoresearch-flows

Conversation

@bussyjd

Copy link
Copy Markdown
Contributor

Summary

Introduces pi-autoresearch integration for automated real user flow validation, plus Go bug fixes and doc corrections discovered during the overnight run.

Commit 1: Flow scripts + autoresearch harness

10 bash flow scripts that exercise every documented user journey from docs/getting-started.md and docs/guides/monetize-inference.md:

flows/
├── lib.sh # Shared harness (step/pass/fail/poll, Anvil credentials)
├── flow-01-prerequisites.sh # Docker, Ollama, obol binary
├── flow-02-stack-init-up.sh # getting-started §1-2
├── flow-03-inference.sh # getting-started §3a-3d (LiteLLM, tool-calls)
├── flow-04-agent.sh # getting-started §4-5 (agent init, inference)
├── flow-05-network.sh # getting-started §6 (network add/remove)
├── flow-06-sell-setup.sh # monetize §1.1-1.4 (pricing, ServiceOffer, heartbeat)
├── flow-07-sell-verify.sh # monetize §1.5-1.7 (tunnel, routes, 402, metrics)
├── flow-08-buy.sh # monetize §2 (discovery, EIP-712 payment, settlement)
├── flow-09-lifecycle.sh # monetize §4 (stop, delete, cleanup)
└── flow-10-anvil-facilitator.sh # monetize §3 (Anvil fork, x402-rs facilitator)

All commands use the real obol binary — no go run, no direct kubectl, no pod IP access.

Commit 2: Bug fixes found by the flow scripts

Timing (closes#280):

  • ensureHeartbeatActive() — heartbeat was stuck at 30m default after obol agent init
  • patchHeartbeatAfterSync()SyncAgentBaseURL helmfile sync was resetting heartbeat config
  • readCurrentAgentBaseURL() — skip sync when tunnel URL unchanged (idempotency)

CLI validation:

  • validateRPCEndpoint() in obol network add — reject invalid URLs

Doc corrections:

  • getting-started §3c-3d: add LiteLLM Bearer auth header (was returning 401)
  • monetize §1.6: correct eRPC path to /rpc/evm/{chainId} (local-only, not via tunnel)
  • monetize §2.1: clarify /skill.md (always available) vs /.well-known (requires --register)

Discovery method

Overnight pi-autoresearch session: 44 → 133 steps passing across 50+ experiments. The agent autonomously ran flow scripts, diagnosed failures, fixed Go code and flow scripts, and re-validated — all while go build/go test acted as backpressure.

Test plan

  • go build ./... passes
  • go test ./internal/{agent,tunnel,network,openclaw,model}/ passes
  • pi-autoresearch: 133/133 steps, 50+ runs
  • Manual: bash flows/flow-01-prerequisites.sh through flow-09-lifecycle.sh on fresh cluster

@bussyjd
bussyjdforce-pushed the feat/autoresearch-flows branch from c040301 to e9683b1CompareMarch 19, 2026 10:03
Introduces pi-autoresearch integration with 10 flow scripts that exercise
every documented user journey from getting-started.md and monetize-inference.md.
Flow scripts (flows/):
- flow-01: prerequisites (Docker, Ollama, obol binary)
- flow-02: stack init + up + verify (getting-started §1-2)
- flow-03: LLM inference chain (getting-started §3a-3d)
- flow-04: agent init + inference (getting-started §4-5)
- flow-05: network add/remove (getting-started §6)
- flow-06: sell setup — pricing, ServiceOffer, heartbeat wait (monetize §1.1-1.4)
- flow-07: sell verify — tunnel, routes, 402, metrics (monetize §1.5-1.7)
- flow-08: buy — discovery, 402 parse, EIP-712 payment, settlement (monetize §2)
- flow-09: lifecycle — stop, delete, cleanup verification (monetize §4)
- flow-10: Anvil fork + x402-rs facilitator setup (monetize §3)
Bug fixes found by the flow scripts:
- ensureHeartbeatActive() — heartbeat stuck at 30m default after obol agent init
- patchHeartbeatAfterSync() — SyncAgentBaseURL helmfile sync was resetting heartbeat
- validateRPCEndpoint() — obol network add was accepting invalid URLs
- Doc corrections: LiteLLM auth, eRPC path, /skill.md vs /.well-known
Test account keys are derived at runtime from the Anvil mnemonic via
`cast wallet private-key` — no private keys in source.
Closes#280
@bussyjd
bussyjdforce-pushed the feat/autoresearch-flows branch from e9683b1 to e56ae6dCompareMarch 19, 2026 10:09
@bussyjd
bussyjd requested a review from OisinKyneMarch 19, 2026 10:20
@bussyjdbussyjd changed the title feat: autoresearch flow scripts + heartbeat/CLI/doc fixesfeat: real user flow validation scripts + heartbeat timing fixesMar 19, 2026

@OisinKyneOisinKyne left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

i'm a little unsure of the accuracy of the flows, at least they imply features i dont expect. I also think we probably need the heartbeat to be agent writable. I tried to get my obolclaw to modify key facts about itself and it was unable to (tuning down a 2 minute heartbeat was one). I don't know if giving it the auth to write to a config map is worth it, if instead we can get these files all mounted into the openclaw pv, and then obol cli just messes with the files on the host (e.g. how obol model sync can change the openclaw agent definition).

To that end, i might pull some of the fixes in separately to this pr, so as not to delay waiting for feedback. (the fact that the flow tests aren't in the user hot path makes me less worried about them being imperfect)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm a little confused by some of the changes in this file (but its a docs so nbd

Comment on lines +44 to +45
# Use qwen3.5:9b — it is configured in LiteLLM's model_list (FLOW_MODEL qwen3:0.6b
# is only registered in Ollama directly; the x402 sell/buy flows use it via that path)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

qwen3:0.6b, should we have a qwen3.5:?

out=$("$OBOL" kubectl exec -n llm deployment/litellm -c litellm -- \
python3 -c "
import urllib.request
r = urllib.request.urlopen('http://ollama.llm.svc.cluster.local:11434/api/tags', timeout=10)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we have internal ollama code? maybe we should internalise a llama.cpp pod

fail "LiteLLM inference failed — ${out:0:300}"
fi

# §3d: Tool-call passthrough

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Tool call at the litellm layer seems like it might confuse things

Comment on lines +147 to +227
func ensureHeartbeatActive(cfg *config.Config, u *ui.UI) error {
namespace := fmt.Sprintf("openclaw-%s", DefaultInstanceID)
kubectlBin := filepath.Join(cfg.BinDir, "kubectl")
kubeconfigPath := filepath.Join(cfg.ConfigDir, "kubeconfig.yaml")
env := append(os.Environ(), fmt.Sprintf("KUBECONFIG=%s", kubeconfigPath))

// Read current ConfigMap.
getCmd := exec.Command(kubectlBin,
"get", "configmap", "openclaw-config",
"-n", namespace,
"-o", "jsonpath={.data.openclaw\\.json}")
getCmd.Env = env
var outBuf bytes.Buffer
getCmd.Stdout = &outBuf
if err := getCmd.Run(); err != nil {
return fmt.Errorf("read openclaw-config: %w", err)
}

var cfgJSON map[string]interface{}
if err := json.Unmarshal(outBuf.Bytes(), &cfgJSON); err != nil {
return fmt.Errorf("parse openclaw.json: %w", err)
}

// Check whether heartbeat is already present.
agents, _ := cfgJSON["agents"].(map[string]interface{})
defaults, _ := agents["defaults"].(map[string]interface{})
_, alreadySet := defaults["heartbeat"]
if alreadySet {
u.Success("Heartbeat config already active")
return nil
}

// Inject heartbeat.
if agents == nil {
agents = map[string]interface{}{}
cfgJSON["agents"] = agents
}
if defaults == nil {
defaults = map[string]interface{}{}
agents["defaults"] = defaults
}
defaults["heartbeat"] = map[string]interface{}{
"every": "5m",
"target": "none",
}

patched, err := json.MarshalIndent(cfgJSON, "", " ")
if err != nil {
return fmt.Errorf("marshal patched config: %w", err)
}

applyPayload := map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "openclaw-config",
"namespace": namespace,
},
"data": map[string]string{
"openclaw.json": string(patched),
},
}
applyRaw, _ := json.Marshal(applyPayload)

applyCmd := exec.Command(kubectlBin,
"apply", "-f", "-",
"--server-side", "--field-manager=helm", "--force-conflicts")
applyCmd.Env = env
applyCmd.Stdin = bytes.NewReader(applyRaw)
var applyErr bytes.Buffer
applyCmd.Stderr = &applyErr
if err := applyCmd.Run(); err != nil {
return fmt.Errorf("patch heartbeat config: %w\n%s", err, applyErr.String())
}

// OpenClaw watches for ConfigMap file changes and hot-reloads config.
// No pod restart is needed: the running pod will detect the update within
// ~30-60s and apply [reload] config hot reload, switching the heartbeat
// interval to 5m immediately without losing the running pod or its state.
u.Success("Heartbeat config injected — OpenClaw hot reload will activate it (every 5m)")
return nil

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Mounting openclaw stuff like this as a config map means its read only to the runtime. This has ux problems because openclaw can't tune stuff for itself. If instead we do this with files on the host maybe they'll be mutable by the openclaw service?

Comment on lines +1 to +3
{
"workingDir": "/Users/bussyjd/Development/Obol_Workbench/obol-stack/.worktrees/autoresearch"
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

remove

Comment threadautoresearch.md
Comment on lines +174 to +177
### Session 1 (baseline → 61/61)

**Baseline: 44/57** — 13 failures across all flows.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should we move this to ./flows or somewhere other than top level if its not for users?

Comment threadautoresearch.sh
@@ -0,0 +1,48 @@
#!/bin/bash

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

subdirectory

@bussyjd

Copy link
Copy Markdown
ContributorAuthor

Closing — core CLI/Go changes were cherry-picked into feat/monetize-path (PR #288). The autoresearch shell scripts and top-level files flagged in @OisinKyne's review (hardcoded paths in autoresearch.config.json, autoresearch.sh/.md at root) were intentionally not carried over. Review feedback addressed by omission.

@OisinKyne
OisinKyne deleted the feat/autoresearch-flows branch April 23, 2026 19:26
Sign up for freeto 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.

fix: heartbeat timing bugs — reset on sell, missing activation, ConfigMap race

2 participants

@bussyjd@OisinKyne
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: real user flow validation scripts + heartbeat timing fixes - #282

Closed
bussyjd wants to merge 1 commit into
mainfrom
feat/autoresearch-flows
Closed

feat: real user flow validation scripts + heartbeat timing fixes#282
bussyjd wants to merge 1 commit into
mainfrom
feat/autoresearch-flows

Conversation

@bussyjd

Copy link
Copy Markdown
Contributor

Summary

Introduces pi-autoresearch integration for automated real user flow validation, plus Go bug fixes and doc corrections discovered during the overnight run.

Commit 1: Flow scripts + autoresearch harness

10 bash flow scripts that exercise every documented user journey from docs/getting-started.md and docs/guides/monetize-inference.md:

flows/
├── lib.sh # Shared harness (step/pass/fail/poll, Anvil credentials)
├── flow-01-prerequisites.sh # Docker, Ollama, obol binary
├── flow-02-stack-init-up.sh # getting-started §1-2
├── flow-03-inference.sh # getting-started §3a-3d (LiteLLM, tool-calls)
├── flow-04-agent.sh # getting-started §4-5 (agent init, inference)
├── flow-05-network.sh # getting-started §6 (network add/remove)
├── flow-06-sell-setup.sh # monetize §1.1-1.4 (pricing, ServiceOffer, heartbeat)
├── flow-07-sell-verify.sh # monetize §1.5-1.7 (tunnel, routes, 402, metrics)
├── flow-08-buy.sh # monetize §2 (discovery, EIP-712 payment, settlement)
├── flow-09-lifecycle.sh # monetize §4 (stop, delete, cleanup)
└── flow-10-anvil-facilitator.sh # monetize §3 (Anvil fork, x402-rs facilitator)

All commands use the real obol binary — no go run, no direct kubectl, no pod IP access.

Commit 2: Bug fixes found by the flow scripts

Timing (closes#280):

  • ensureHeartbeatActive() — heartbeat was stuck at 30m default after obol agent init
  • patchHeartbeatAfterSync()SyncAgentBaseURL helmfile sync was resetting heartbeat config
  • readCurrentAgentBaseURL() — skip sync when tunnel URL unchanged (idempotency)

CLI validation:

  • validateRPCEndpoint() in obol network add — reject invalid URLs

Doc corrections:

  • getting-started §3c-3d: add LiteLLM Bearer auth header (was returning 401)
  • monetize §1.6: correct eRPC path to /rpc/evm/{chainId} (local-only, not via tunnel)
  • monetize §2.1: clarify /skill.md (always available) vs /.well-known (requires --register)

Discovery method

Overnight pi-autoresearch session: 44 → 133 steps passing across 50+ experiments. The agent autonomously ran flow scripts, diagnosed failures, fixed Go code and flow scripts, and re-validated — all while go build/go test acted as backpressure.

Test plan

  • go build ./... passes
  • go test ./internal/{agent,tunnel,network,openclaw,model}/ passes
  • pi-autoresearch: 133/133 steps, 50+ runs
  • Manual: bash flows/flow-01-prerequisites.sh through flow-09-lifecycle.sh on fresh cluster

@bussyjd
bussyjdforce-pushed the feat/autoresearch-flows branch from c040301 to e9683b1CompareMarch 19, 2026 10:03
Introduces pi-autoresearch integration with 10 flow scripts that exercise
every documented user journey from getting-started.md and monetize-inference.md.
Flow scripts (flows/):
- flow-01: prerequisites (Docker, Ollama, obol binary)
- flow-02: stack init + up + verify (getting-started §1-2)
- flow-03: LLM inference chain (getting-started §3a-3d)
- flow-04: agent init + inference (getting-started §4-5)
- flow-05: network add/remove (getting-started §6)
- flow-06: sell setup — pricing, ServiceOffer, heartbeat wait (monetize §1.1-1.4)
- flow-07: sell verify — tunnel, routes, 402, metrics (monetize §1.5-1.7)
- flow-08: buy — discovery, 402 parse, EIP-712 payment, settlement (monetize §2)
- flow-09: lifecycle — stop, delete, cleanup verification (monetize §4)
- flow-10: Anvil fork + x402-rs facilitator setup (monetize §3)
Bug fixes found by the flow scripts:
- ensureHeartbeatActive() — heartbeat stuck at 30m default after obol agent init
- patchHeartbeatAfterSync() — SyncAgentBaseURL helmfile sync was resetting heartbeat
- validateRPCEndpoint() — obol network add was accepting invalid URLs
- Doc corrections: LiteLLM auth, eRPC path, /skill.md vs /.well-known
Test account keys are derived at runtime from the Anvil mnemonic via
`cast wallet private-key` — no private keys in source.
Closes#280
@bussyjd
bussyjdforce-pushed the feat/autoresearch-flows branch from e9683b1 to e56ae6dCompareMarch 19, 2026 10:09
@bussyjd
bussyjd requested a review from OisinKyneMarch 19, 2026 10:20
@bussyjdbussyjd changed the title feat: autoresearch flow scripts + heartbeat/CLI/doc fixesfeat: real user flow validation scripts + heartbeat timing fixesMar 19, 2026

@OisinKyneOisinKyne left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

i'm a little unsure of the accuracy of the flows, at least they imply features i dont expect. I also think we probably need the heartbeat to be agent writable. I tried to get my obolclaw to modify key facts about itself and it was unable to (tuning down a 2 minute heartbeat was one). I don't know if giving it the auth to write to a config map is worth it, if instead we can get these files all mounted into the openclaw pv, and then obol cli just messes with the files on the host (e.g. how obol model sync can change the openclaw agent definition).

To that end, i might pull some of the fixes in separately to this pr, so as not to delay waiting for feedback. (the fact that the flow tests aren't in the user hot path makes me less worried about them being imperfect)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm a little confused by some of the changes in this file (but its a docs so nbd

Comment on lines +44 to +45
# Use qwen3.5:9b — it is configured in LiteLLM's model_list (FLOW_MODEL qwen3:0.6b
# is only registered in Ollama directly; the x402 sell/buy flows use it via that path)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

qwen3:0.6b, should we have a qwen3.5:?

out=$("$OBOL" kubectl exec -n llm deployment/litellm -c litellm -- \
python3 -c "
import urllib.request
r = urllib.request.urlopen('http://ollama.llm.svc.cluster.local:11434/api/tags', timeout=10)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we have internal ollama code? maybe we should internalise a llama.cpp pod

fail "LiteLLM inference failed — ${out:0:300}"
fi

# §3d: Tool-call passthrough

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Tool call at the litellm layer seems like it might confuse things

Comment on lines +147 to +227
func ensureHeartbeatActive(cfg *config.Config, u *ui.UI) error {
namespace := fmt.Sprintf("openclaw-%s", DefaultInstanceID)
kubectlBin := filepath.Join(cfg.BinDir, "kubectl")
kubeconfigPath := filepath.Join(cfg.ConfigDir, "kubeconfig.yaml")
env := append(os.Environ(), fmt.Sprintf("KUBECONFIG=%s", kubeconfigPath))

// Read current ConfigMap.
getCmd := exec.Command(kubectlBin,
"get", "configmap", "openclaw-config",
"-n", namespace,
"-o", "jsonpath={.data.openclaw\\.json}")
getCmd.Env = env
var outBuf bytes.Buffer
getCmd.Stdout = &outBuf
if err := getCmd.Run(); err != nil {
return fmt.Errorf("read openclaw-config: %w", err)
}

var cfgJSON map[string]interface{}
if err := json.Unmarshal(outBuf.Bytes(), &cfgJSON); err != nil {
return fmt.Errorf("parse openclaw.json: %w", err)
}

// Check whether heartbeat is already present.
agents, _ := cfgJSON["agents"].(map[string]interface{})
defaults, _ := agents["defaults"].(map[string]interface{})
_, alreadySet := defaults["heartbeat"]
if alreadySet {
u.Success("Heartbeat config already active")
return nil
}

// Inject heartbeat.
if agents == nil {
agents = map[string]interface{}{}
cfgJSON["agents"] = agents
}
if defaults == nil {
defaults = map[string]interface{}{}
agents["defaults"] = defaults
}
defaults["heartbeat"] = map[string]interface{}{
"every": "5m",
"target": "none",
}

patched, err := json.MarshalIndent(cfgJSON, "", " ")
if err != nil {
return fmt.Errorf("marshal patched config: %w", err)
}

applyPayload := map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "openclaw-config",
"namespace": namespace,
},
"data": map[string]string{
"openclaw.json": string(patched),
},
}
applyRaw, _ := json.Marshal(applyPayload)

applyCmd := exec.Command(kubectlBin,
"apply", "-f", "-",
"--server-side", "--field-manager=helm", "--force-conflicts")
applyCmd.Env = env
applyCmd.Stdin = bytes.NewReader(applyRaw)
var applyErr bytes.Buffer
applyCmd.Stderr = &applyErr
if err := applyCmd.Run(); err != nil {
return fmt.Errorf("patch heartbeat config: %w\n%s", err, applyErr.String())
}

// OpenClaw watches for ConfigMap file changes and hot-reloads config.
// No pod restart is needed: the running pod will detect the update within
// ~30-60s and apply [reload] config hot reload, switching the heartbeat
// interval to 5m immediately without losing the running pod or its state.
u.Success("Heartbeat config injected — OpenClaw hot reload will activate it (every 5m)")
return nil

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Mounting openclaw stuff like this as a config map means its read only to the runtime. This has ux problems because openclaw can't tune stuff for itself. If instead we do this with files on the host maybe they'll be mutable by the openclaw service?

Comment on lines +1 to +3
{
"workingDir": "/Users/bussyjd/Development/Obol_Workbench/obol-stack/.worktrees/autoresearch"
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

remove

Comment threadautoresearch.md
Comment on lines +174 to +177
### Session 1 (baseline → 61/61)

**Baseline: 44/57** — 13 failures across all flows.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should we move this to ./flows or somewhere other than top level if its not for users?

Comment threadautoresearch.sh
@@ -0,0 +1,48 @@
#!/bin/bash

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

subdirectory

@bussyjd

Copy link
Copy Markdown
ContributorAuthor

Closing — core CLI/Go changes were cherry-picked into feat/monetize-path (PR #288). The autoresearch shell scripts and top-level files flagged in @OisinKyne's review (hardcoded paths in autoresearch.config.json, autoresearch.sh/.md at root) were intentionally not carried over. Review feedback addressed by omission.

@OisinKyne
OisinKyne deleted the feat/autoresearch-flows branch April 23, 2026 19:26
Sign up for freeto 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.

fix: heartbeat timing bugs — reset on sell, missing activation, ConfigMap race

2 participants

@bussyjd@OisinKyne
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat: real user flow validation scripts + heartbeat timing fixes - #282

Closed
bussyjd wants to merge 1 commit into
mainfrom
feat/autoresearch-flows
Closed

feat: real user flow validation scripts + heartbeat timing fixes#282
bussyjd wants to merge 1 commit into
mainfrom
feat/autoresearch-flows

Conversation

@bussyjd

Copy link
Copy Markdown
Contributor

Summary

Introduces pi-autoresearch integration for automated real user flow validation, plus Go bug fixes and doc corrections discovered during the overnight run.

Commit 1: Flow scripts + autoresearch harness

10 bash flow scripts that exercise every documented user journey from docs/getting-started.md and docs/guides/monetize-inference.md:

flows/
├── lib.sh # Shared harness (step/pass/fail/poll, Anvil credentials)
├── flow-01-prerequisites.sh # Docker, Ollama, obol binary
├── flow-02-stack-init-up.sh # getting-started §1-2
├── flow-03-inference.sh # getting-started §3a-3d (LiteLLM, tool-calls)
├── flow-04-agent.sh # getting-started §4-5 (agent init, inference)
├── flow-05-network.sh # getting-started §6 (network add/remove)
├── flow-06-sell-setup.sh # monetize §1.1-1.4 (pricing, ServiceOffer, heartbeat)
├── flow-07-sell-verify.sh # monetize §1.5-1.7 (tunnel, routes, 402, metrics)
├── flow-08-buy.sh # monetize §2 (discovery, EIP-712 payment, settlement)
├── flow-09-lifecycle.sh # monetize §4 (stop, delete, cleanup)
└── flow-10-anvil-facilitator.sh # monetize §3 (Anvil fork, x402-rs facilitator)

All commands use the real obol binary — no go run, no direct kubectl, no pod IP access.

Commit 2: Bug fixes found by the flow scripts

Timing (closes#280):

  • ensureHeartbeatActive() — heartbeat was stuck at 30m default after obol agent init
  • patchHeartbeatAfterSync()SyncAgentBaseURL helmfile sync was resetting heartbeat config
  • readCurrentAgentBaseURL() — skip sync when tunnel URL unchanged (idempotency)

CLI validation:

  • validateRPCEndpoint() in obol network add — reject invalid URLs

Doc corrections:

  • getting-started §3c-3d: add LiteLLM Bearer auth header (was returning 401)
  • monetize §1.6: correct eRPC path to /rpc/evm/{chainId} (local-only, not via tunnel)
  • monetize §2.1: clarify /skill.md (always available) vs /.well-known (requires --register)

Discovery method

Overnight pi-autoresearch session: 44 → 133 steps passing across 50+ experiments. The agent autonomously ran flow scripts, diagnosed failures, fixed Go code and flow scripts, and re-validated — all while go build/go test acted as backpressure.

Test plan

  • go build ./... passes
  • go test ./internal/{agent,tunnel,network,openclaw,model}/ passes
  • pi-autoresearch: 133/133 steps, 50+ runs
  • Manual: bash flows/flow-01-prerequisites.sh through flow-09-lifecycle.sh on fresh cluster

@bussyjd
bussyjdforce-pushed the feat/autoresearch-flows branch from c040301 to e9683b1CompareMarch 19, 2026 10:03
Introduces pi-autoresearch integration with 10 flow scripts that exercise
every documented user journey from getting-started.md and monetize-inference.md.
Flow scripts (flows/):
- flow-01: prerequisites (Docker, Ollama, obol binary)
- flow-02: stack init + up + verify (getting-started §1-2)
- flow-03: LLM inference chain (getting-started §3a-3d)
- flow-04: agent init + inference (getting-started §4-5)
- flow-05: network add/remove (getting-started §6)
- flow-06: sell setup — pricing, ServiceOffer, heartbeat wait (monetize §1.1-1.4)
- flow-07: sell verify — tunnel, routes, 402, metrics (monetize §1.5-1.7)
- flow-08: buy — discovery, 402 parse, EIP-712 payment, settlement (monetize §2)
- flow-09: lifecycle — stop, delete, cleanup verification (monetize §4)
- flow-10: Anvil fork + x402-rs facilitator setup (monetize §3)
Bug fixes found by the flow scripts:
- ensureHeartbeatActive() — heartbeat stuck at 30m default after obol agent init
- patchHeartbeatAfterSync() — SyncAgentBaseURL helmfile sync was resetting heartbeat
- validateRPCEndpoint() — obol network add was accepting invalid URLs
- Doc corrections: LiteLLM auth, eRPC path, /skill.md vs /.well-known
Test account keys are derived at runtime from the Anvil mnemonic via
`cast wallet private-key` — no private keys in source.
Closes#280
@bussyjd
bussyjdforce-pushed the feat/autoresearch-flows branch from e9683b1 to e56ae6dCompareMarch 19, 2026 10:09
@bussyjd
bussyjd requested a review from OisinKyneMarch 19, 2026 10:20
@bussyjdbussyjd changed the title feat: autoresearch flow scripts + heartbeat/CLI/doc fixesfeat: real user flow validation scripts + heartbeat timing fixesMar 19, 2026

@OisinKyneOisinKyne left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

i'm a little unsure of the accuracy of the flows, at least they imply features i dont expect. I also think we probably need the heartbeat to be agent writable. I tried to get my obolclaw to modify key facts about itself and it was unable to (tuning down a 2 minute heartbeat was one). I don't know if giving it the auth to write to a config map is worth it, if instead we can get these files all mounted into the openclaw pv, and then obol cli just messes with the files on the host (e.g. how obol model sync can change the openclaw agent definition).

To that end, i might pull some of the fixes in separately to this pr, so as not to delay waiting for feedback. (the fact that the flow tests aren't in the user hot path makes me less worried about them being imperfect)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm a little confused by some of the changes in this file (but its a docs so nbd

Comment on lines +44 to +45
# Use qwen3.5:9b — it is configured in LiteLLM's model_list (FLOW_MODEL qwen3:0.6b
# is only registered in Ollama directly; the x402 sell/buy flows use it via that path)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

qwen3:0.6b, should we have a qwen3.5:?

out=$("$OBOL" kubectl exec -n llm deployment/litellm -c litellm -- \
python3 -c "
import urllib.request
r = urllib.request.urlopen('http://ollama.llm.svc.cluster.local:11434/api/tags', timeout=10)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we have internal ollama code? maybe we should internalise a llama.cpp pod

fail "LiteLLM inference failed — ${out:0:300}"
fi

# §3d: Tool-call passthrough

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Tool call at the litellm layer seems like it might confuse things

Comment on lines +147 to +227
func ensureHeartbeatActive(cfg *config.Config, u *ui.UI) error {
namespace := fmt.Sprintf("openclaw-%s", DefaultInstanceID)
kubectlBin := filepath.Join(cfg.BinDir, "kubectl")
kubeconfigPath := filepath.Join(cfg.ConfigDir, "kubeconfig.yaml")
env := append(os.Environ(), fmt.Sprintf("KUBECONFIG=%s", kubeconfigPath))

// Read current ConfigMap.
getCmd := exec.Command(kubectlBin,
"get", "configmap", "openclaw-config",
"-n", namespace,
"-o", "jsonpath={.data.openclaw\\.json}")
getCmd.Env = env
var outBuf bytes.Buffer
getCmd.Stdout = &outBuf
if err := getCmd.Run(); err != nil {
return fmt.Errorf("read openclaw-config: %w", err)
}

var cfgJSON map[string]interface{}
if err := json.Unmarshal(outBuf.Bytes(), &cfgJSON); err != nil {
return fmt.Errorf("parse openclaw.json: %w", err)
}

// Check whether heartbeat is already present.
agents, _ := cfgJSON["agents"].(map[string]interface{})
defaults, _ := agents["defaults"].(map[string]interface{})
_, alreadySet := defaults["heartbeat"]
if alreadySet {
u.Success("Heartbeat config already active")
return nil
}

// Inject heartbeat.
if agents == nil {
agents = map[string]interface{}{}
cfgJSON["agents"] = agents
}
if defaults == nil {
defaults = map[string]interface{}{}
agents["defaults"] = defaults
}
defaults["heartbeat"] = map[string]interface{}{
"every": "5m",
"target": "none",
}

patched, err := json.MarshalIndent(cfgJSON, "", " ")
if err != nil {
return fmt.Errorf("marshal patched config: %w", err)
}

applyPayload := map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "openclaw-config",
"namespace": namespace,
},
"data": map[string]string{
"openclaw.json": string(patched),
},
}
applyRaw, _ := json.Marshal(applyPayload)

applyCmd := exec.Command(kubectlBin,
"apply", "-f", "-",
"--server-side", "--field-manager=helm", "--force-conflicts")
applyCmd.Env = env
applyCmd.Stdin = bytes.NewReader(applyRaw)
var applyErr bytes.Buffer
applyCmd.Stderr = &applyErr
if err := applyCmd.Run(); err != nil {
return fmt.Errorf("patch heartbeat config: %w\n%s", err, applyErr.String())
}

// OpenClaw watches for ConfigMap file changes and hot-reloads config.
// No pod restart is needed: the running pod will detect the update within
// ~30-60s and apply [reload] config hot reload, switching the heartbeat
// interval to 5m immediately without losing the running pod or its state.
u.Success("Heartbeat config injected — OpenClaw hot reload will activate it (every 5m)")
return nil

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Mounting openclaw stuff like this as a config map means its read only to the runtime. This has ux problems because openclaw can't tune stuff for itself. If instead we do this with files on the host maybe they'll be mutable by the openclaw service?

Comment on lines +1 to +3
{
"workingDir": "/Users/bussyjd/Development/Obol_Workbench/obol-stack/.worktrees/autoresearch"
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

remove

Comment threadautoresearch.md
Comment on lines +174 to +177
### Session 1 (baseline → 61/61)

**Baseline: 44/57** — 13 failures across all flows.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should we move this to ./flows or somewhere other than top level if its not for users?

Comment threadautoresearch.sh
@@ -0,0 +1,48 @@
#!/bin/bash

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

subdirectory

@bussyjd

Copy link
Copy Markdown
ContributorAuthor

Closing — core CLI/Go changes were cherry-picked into feat/monetize-path (PR #288). The autoresearch shell scripts and top-level files flagged in @OisinKyne's review (hardcoded paths in autoresearch.config.json, autoresearch.sh/.md at root) were intentionally not carried over. Review feedback addressed by omission.

@OisinKyne
OisinKyne deleted the feat/autoresearch-flows branch April 23, 2026 19:26
Sign up for freeto 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.

fix: heartbeat timing bugs — reset on sell, missing activation, ConfigMap race

2 participants

@bussyjd@OisinKyne
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: real user flow validation scripts + heartbeat timing fixes - #282

Closed
bussyjd wants to merge 1 commit into
mainfrom
feat/autoresearch-flows
Closed

feat: real user flow validation scripts + heartbeat timing fixes#282
bussyjd wants to merge 1 commit into
mainfrom
feat/autoresearch-flows

Conversation

@bussyjd

Copy link
Copy Markdown
Contributor

Summary

Introduces pi-autoresearch integration for automated real user flow validation, plus Go bug fixes and doc corrections discovered during the overnight run.

Commit 1: Flow scripts + autoresearch harness

10 bash flow scripts that exercise every documented user journey from docs/getting-started.md and docs/guides/monetize-inference.md:

flows/
├── lib.sh # Shared harness (step/pass/fail/poll, Anvil credentials)
├── flow-01-prerequisites.sh # Docker, Ollama, obol binary
├── flow-02-stack-init-up.sh # getting-started §1-2
├── flow-03-inference.sh # getting-started §3a-3d (LiteLLM, tool-calls)
├── flow-04-agent.sh # getting-started §4-5 (agent init, inference)
├── flow-05-network.sh # getting-started §6 (network add/remove)
├── flow-06-sell-setup.sh # monetize §1.1-1.4 (pricing, ServiceOffer, heartbeat)
├── flow-07-sell-verify.sh # monetize §1.5-1.7 (tunnel, routes, 402, metrics)
├── flow-08-buy.sh # monetize §2 (discovery, EIP-712 payment, settlement)
├── flow-09-lifecycle.sh # monetize §4 (stop, delete, cleanup)
└── flow-10-anvil-facilitator.sh # monetize §3 (Anvil fork, x402-rs facilitator)

All commands use the real obol binary — no go run, no direct kubectl, no pod IP access.

Commit 2: Bug fixes found by the flow scripts

Timing (closes#280):

  • ensureHeartbeatActive() — heartbeat was stuck at 30m default after obol agent init
  • patchHeartbeatAfterSync()SyncAgentBaseURL helmfile sync was resetting heartbeat config
  • readCurrentAgentBaseURL() — skip sync when tunnel URL unchanged (idempotency)

CLI validation:

  • validateRPCEndpoint() in obol network add — reject invalid URLs

Doc corrections:

  • getting-started §3c-3d: add LiteLLM Bearer auth header (was returning 401)
  • monetize §1.6: correct eRPC path to /rpc/evm/{chainId} (local-only, not via tunnel)
  • monetize §2.1: clarify /skill.md (always available) vs /.well-known (requires --register)

Discovery method

Overnight pi-autoresearch session: 44 → 133 steps passing across 50+ experiments. The agent autonomously ran flow scripts, diagnosed failures, fixed Go code and flow scripts, and re-validated — all while go build/go test acted as backpressure.

Test plan

  • go build ./... passes
  • go test ./internal/{agent,tunnel,network,openclaw,model}/ passes
  • pi-autoresearch: 133/133 steps, 50+ runs
  • Manual: bash flows/flow-01-prerequisites.sh through flow-09-lifecycle.sh on fresh cluster

@bussyjd
bussyjdforce-pushed the feat/autoresearch-flows branch from c040301 to e9683b1CompareMarch 19, 2026 10:03
Introduces pi-autoresearch integration with 10 flow scripts that exercise
every documented user journey from getting-started.md and monetize-inference.md.
Flow scripts (flows/):
- flow-01: prerequisites (Docker, Ollama, obol binary)
- flow-02: stack init + up + verify (getting-started §1-2)
- flow-03: LLM inference chain (getting-started §3a-3d)
- flow-04: agent init + inference (getting-started §4-5)
- flow-05: network add/remove (getting-started §6)
- flow-06: sell setup — pricing, ServiceOffer, heartbeat wait (monetize §1.1-1.4)
- flow-07: sell verify — tunnel, routes, 402, metrics (monetize §1.5-1.7)
- flow-08: buy — discovery, 402 parse, EIP-712 payment, settlement (monetize §2)
- flow-09: lifecycle — stop, delete, cleanup verification (monetize §4)
- flow-10: Anvil fork + x402-rs facilitator setup (monetize §3)
Bug fixes found by the flow scripts:
- ensureHeartbeatActive() — heartbeat stuck at 30m default after obol agent init
- patchHeartbeatAfterSync() — SyncAgentBaseURL helmfile sync was resetting heartbeat
- validateRPCEndpoint() — obol network add was accepting invalid URLs
- Doc corrections: LiteLLM auth, eRPC path, /skill.md vs /.well-known
Test account keys are derived at runtime from the Anvil mnemonic via
`cast wallet private-key` — no private keys in source.
Closes#280
@bussyjd
bussyjdforce-pushed the feat/autoresearch-flows branch from e9683b1 to e56ae6dCompareMarch 19, 2026 10:09
@bussyjd
bussyjd requested a review from OisinKyneMarch 19, 2026 10:20
@bussyjdbussyjd changed the title feat: autoresearch flow scripts + heartbeat/CLI/doc fixesfeat: real user flow validation scripts + heartbeat timing fixesMar 19, 2026

@OisinKyneOisinKyne left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

i'm a little unsure of the accuracy of the flows, at least they imply features i dont expect. I also think we probably need the heartbeat to be agent writable. I tried to get my obolclaw to modify key facts about itself and it was unable to (tuning down a 2 minute heartbeat was one). I don't know if giving it the auth to write to a config map is worth it, if instead we can get these files all mounted into the openclaw pv, and then obol cli just messes with the files on the host (e.g. how obol model sync can change the openclaw agent definition).

To that end, i might pull some of the fixes in separately to this pr, so as not to delay waiting for feedback. (the fact that the flow tests aren't in the user hot path makes me less worried about them being imperfect)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm a little confused by some of the changes in this file (but its a docs so nbd

Comment on lines +44 to +45
# Use qwen3.5:9b — it is configured in LiteLLM's model_list (FLOW_MODEL qwen3:0.6b
# is only registered in Ollama directly; the x402 sell/buy flows use it via that path)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

qwen3:0.6b, should we have a qwen3.5:?

out=$("$OBOL" kubectl exec -n llm deployment/litellm -c litellm -- \
python3 -c "
import urllib.request
r = urllib.request.urlopen('http://ollama.llm.svc.cluster.local:11434/api/tags', timeout=10)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we have internal ollama code? maybe we should internalise a llama.cpp pod

fail "LiteLLM inference failed — ${out:0:300}"
fi

# §3d: Tool-call passthrough

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Tool call at the litellm layer seems like it might confuse things

Comment on lines +147 to +227
func ensureHeartbeatActive(cfg *config.Config, u *ui.UI) error {
namespace := fmt.Sprintf("openclaw-%s", DefaultInstanceID)
kubectlBin := filepath.Join(cfg.BinDir, "kubectl")
kubeconfigPath := filepath.Join(cfg.ConfigDir, "kubeconfig.yaml")
env := append(os.Environ(), fmt.Sprintf("KUBECONFIG=%s", kubeconfigPath))

// Read current ConfigMap.
getCmd := exec.Command(kubectlBin,
"get", "configmap", "openclaw-config",
"-n", namespace,
"-o", "jsonpath={.data.openclaw\\.json}")
getCmd.Env = env
var outBuf bytes.Buffer
getCmd.Stdout = &outBuf
if err := getCmd.Run(); err != nil {
return fmt.Errorf("read openclaw-config: %w", err)
}

var cfgJSON map[string]interface{}
if err := json.Unmarshal(outBuf.Bytes(), &cfgJSON); err != nil {
return fmt.Errorf("parse openclaw.json: %w", err)
}

// Check whether heartbeat is already present.
agents, _ := cfgJSON["agents"].(map[string]interface{})
defaults, _ := agents["defaults"].(map[string]interface{})
_, alreadySet := defaults["heartbeat"]
if alreadySet {
u.Success("Heartbeat config already active")
return nil
}

// Inject heartbeat.
if agents == nil {
agents = map[string]interface{}{}
cfgJSON["agents"] = agents
}
if defaults == nil {
defaults = map[string]interface{}{}
agents["defaults"] = defaults
}
defaults["heartbeat"] = map[string]interface{}{
"every": "5m",
"target": "none",
}

patched, err := json.MarshalIndent(cfgJSON, "", " ")
if err != nil {
return fmt.Errorf("marshal patched config: %w", err)
}

applyPayload := map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "openclaw-config",
"namespace": namespace,
},
"data": map[string]string{
"openclaw.json": string(patched),
},
}
applyRaw, _ := json.Marshal(applyPayload)

applyCmd := exec.Command(kubectlBin,
"apply", "-f", "-",
"--server-side", "--field-manager=helm", "--force-conflicts")
applyCmd.Env = env
applyCmd.Stdin = bytes.NewReader(applyRaw)
var applyErr bytes.Buffer
applyCmd.Stderr = &applyErr
if err := applyCmd.Run(); err != nil {
return fmt.Errorf("patch heartbeat config: %w\n%s", err, applyErr.String())
}

// OpenClaw watches for ConfigMap file changes and hot-reloads config.
// No pod restart is needed: the running pod will detect the update within
// ~30-60s and apply [reload] config hot reload, switching the heartbeat
// interval to 5m immediately without losing the running pod or its state.
u.Success("Heartbeat config injected — OpenClaw hot reload will activate it (every 5m)")
return nil

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Mounting openclaw stuff like this as a config map means its read only to the runtime. This has ux problems because openclaw can't tune stuff for itself. If instead we do this with files on the host maybe they'll be mutable by the openclaw service?

Comment on lines +1 to +3
{
"workingDir": "/Users/bussyjd/Development/Obol_Workbench/obol-stack/.worktrees/autoresearch"
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

remove

Comment threadautoresearch.md
Comment on lines +174 to +177
### Session 1 (baseline → 61/61)

**Baseline: 44/57** — 13 failures across all flows.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should we move this to ./flows or somewhere other than top level if its not for users?

Comment threadautoresearch.sh
@@ -0,0 +1,48 @@
#!/bin/bash

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

subdirectory

@bussyjd

Copy link
Copy Markdown
ContributorAuthor

Closing — core CLI/Go changes were cherry-picked into feat/monetize-path (PR #288). The autoresearch shell scripts and top-level files flagged in @OisinKyne's review (hardcoded paths in autoresearch.config.json, autoresearch.sh/.md at root) were intentionally not carried over. Review feedback addressed by omission.

@OisinKyne
OisinKyne deleted the feat/autoresearch-flows branch April 23, 2026 19:26
Sign up for freeto 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.

fix: heartbeat timing bugs — reset on sell, missing activation, ConfigMap race

2 participants

@bussyjd@OisinKyne
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: real user flow validation scripts + heartbeat timing fixes - #282

Closed
bussyjd wants to merge 1 commit into
mainfrom
feat/autoresearch-flows
Closed

feat: real user flow validation scripts + heartbeat timing fixes#282
bussyjd wants to merge 1 commit into
mainfrom
feat/autoresearch-flows

Conversation

@bussyjd

Copy link
Copy Markdown
Contributor

Summary

Introduces pi-autoresearch integration for automated real user flow validation, plus Go bug fixes and doc corrections discovered during the overnight run.

Commit 1: Flow scripts + autoresearch harness

10 bash flow scripts that exercise every documented user journey from docs/getting-started.md and docs/guides/monetize-inference.md:

flows/
├── lib.sh # Shared harness (step/pass/fail/poll, Anvil credentials)
├── flow-01-prerequisites.sh # Docker, Ollama, obol binary
├── flow-02-stack-init-up.sh # getting-started §1-2
├── flow-03-inference.sh # getting-started §3a-3d (LiteLLM, tool-calls)
├── flow-04-agent.sh # getting-started §4-5 (agent init, inference)
├── flow-05-network.sh # getting-started §6 (network add/remove)
├── flow-06-sell-setup.sh # monetize §1.1-1.4 (pricing, ServiceOffer, heartbeat)
├── flow-07-sell-verify.sh # monetize §1.5-1.7 (tunnel, routes, 402, metrics)
├── flow-08-buy.sh # monetize §2 (discovery, EIP-712 payment, settlement)
├── flow-09-lifecycle.sh # monetize §4 (stop, delete, cleanup)
└── flow-10-anvil-facilitator.sh # monetize §3 (Anvil fork, x402-rs facilitator)

All commands use the real obol binary — no go run, no direct kubectl, no pod IP access.

Commit 2: Bug fixes found by the flow scripts

Timing (closes#280):

  • ensureHeartbeatActive() — heartbeat was stuck at 30m default after obol agent init
  • patchHeartbeatAfterSync()SyncAgentBaseURL helmfile sync was resetting heartbeat config
  • readCurrentAgentBaseURL() — skip sync when tunnel URL unchanged (idempotency)

CLI validation:

  • validateRPCEndpoint() in obol network add — reject invalid URLs

Doc corrections:

  • getting-started §3c-3d: add LiteLLM Bearer auth header (was returning 401)
  • monetize §1.6: correct eRPC path to /rpc/evm/{chainId} (local-only, not via tunnel)
  • monetize §2.1: clarify /skill.md (always available) vs /.well-known (requires --register)

Discovery method

Overnight pi-autoresearch session: 44 → 133 steps passing across 50+ experiments. The agent autonomously ran flow scripts, diagnosed failures, fixed Go code and flow scripts, and re-validated — all while go build/go test acted as backpressure.

Test plan

  • go build ./... passes
  • go test ./internal/{agent,tunnel,network,openclaw,model}/ passes
  • pi-autoresearch: 133/133 steps, 50+ runs
  • Manual: bash flows/flow-01-prerequisites.sh through flow-09-lifecycle.sh on fresh cluster

@bussyjd
bussyjdforce-pushed the feat/autoresearch-flows branch from c040301 to e9683b1CompareMarch 19, 2026 10:03
Introduces pi-autoresearch integration with 10 flow scripts that exercise
every documented user journey from getting-started.md and monetize-inference.md.
Flow scripts (flows/):
- flow-01: prerequisites (Docker, Ollama, obol binary)
- flow-02: stack init + up + verify (getting-started §1-2)
- flow-03: LLM inference chain (getting-started §3a-3d)
- flow-04: agent init + inference (getting-started §4-5)
- flow-05: network add/remove (getting-started §6)
- flow-06: sell setup — pricing, ServiceOffer, heartbeat wait (monetize §1.1-1.4)
- flow-07: sell verify — tunnel, routes, 402, metrics (monetize §1.5-1.7)
- flow-08: buy — discovery, 402 parse, EIP-712 payment, settlement (monetize §2)
- flow-09: lifecycle — stop, delete, cleanup verification (monetize §4)
- flow-10: Anvil fork + x402-rs facilitator setup (monetize §3)
Bug fixes found by the flow scripts:
- ensureHeartbeatActive() — heartbeat stuck at 30m default after obol agent init
- patchHeartbeatAfterSync() — SyncAgentBaseURL helmfile sync was resetting heartbeat
- validateRPCEndpoint() — obol network add was accepting invalid URLs
- Doc corrections: LiteLLM auth, eRPC path, /skill.md vs /.well-known
Test account keys are derived at runtime from the Anvil mnemonic via
`cast wallet private-key` — no private keys in source.
Closes#280
@bussyjd
bussyjdforce-pushed the feat/autoresearch-flows branch from e9683b1 to e56ae6dCompareMarch 19, 2026 10:09
@bussyjd
bussyjd requested a review from OisinKyneMarch 19, 2026 10:20
@bussyjdbussyjd changed the title feat: autoresearch flow scripts + heartbeat/CLI/doc fixesfeat: real user flow validation scripts + heartbeat timing fixesMar 19, 2026

@OisinKyneOisinKyne left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

i'm a little unsure of the accuracy of the flows, at least they imply features i dont expect. I also think we probably need the heartbeat to be agent writable. I tried to get my obolclaw to modify key facts about itself and it was unable to (tuning down a 2 minute heartbeat was one). I don't know if giving it the auth to write to a config map is worth it, if instead we can get these files all mounted into the openclaw pv, and then obol cli just messes with the files on the host (e.g. how obol model sync can change the openclaw agent definition).

To that end, i might pull some of the fixes in separately to this pr, so as not to delay waiting for feedback. (the fact that the flow tests aren't in the user hot path makes me less worried about them being imperfect)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm a little confused by some of the changes in this file (but its a docs so nbd

Comment on lines +44 to +45
# Use qwen3.5:9b — it is configured in LiteLLM's model_list (FLOW_MODEL qwen3:0.6b
# is only registered in Ollama directly; the x402 sell/buy flows use it via that path)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

qwen3:0.6b, should we have a qwen3.5:?

out=$("$OBOL" kubectl exec -n llm deployment/litellm -c litellm -- \
python3 -c "
import urllib.request
r = urllib.request.urlopen('http://ollama.llm.svc.cluster.local:11434/api/tags', timeout=10)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we have internal ollama code? maybe we should internalise a llama.cpp pod

fail "LiteLLM inference failed — ${out:0:300}"
fi

# §3d: Tool-call passthrough

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Tool call at the litellm layer seems like it might confuse things

Comment on lines +147 to +227
func ensureHeartbeatActive(cfg *config.Config, u *ui.UI) error {
namespace := fmt.Sprintf("openclaw-%s", DefaultInstanceID)
kubectlBin := filepath.Join(cfg.BinDir, "kubectl")
kubeconfigPath := filepath.Join(cfg.ConfigDir, "kubeconfig.yaml")
env := append(os.Environ(), fmt.Sprintf("KUBECONFIG=%s", kubeconfigPath))

// Read current ConfigMap.
getCmd := exec.Command(kubectlBin,
"get", "configmap", "openclaw-config",
"-n", namespace,
"-o", "jsonpath={.data.openclaw\\.json}")
getCmd.Env = env
var outBuf bytes.Buffer
getCmd.Stdout = &outBuf
if err := getCmd.Run(); err != nil {
return fmt.Errorf("read openclaw-config: %w", err)
}

var cfgJSON map[string]interface{}
if err := json.Unmarshal(outBuf.Bytes(), &cfgJSON); err != nil {
return fmt.Errorf("parse openclaw.json: %w", err)
}

// Check whether heartbeat is already present.
agents, _ := cfgJSON["agents"].(map[string]interface{})
defaults, _ := agents["defaults"].(map[string]interface{})
_, alreadySet := defaults["heartbeat"]
if alreadySet {
u.Success("Heartbeat config already active")
return nil
}

// Inject heartbeat.
if agents == nil {
agents = map[string]interface{}{}
cfgJSON["agents"] = agents
}
if defaults == nil {
defaults = map[string]interface{}{}
agents["defaults"] = defaults
}
defaults["heartbeat"] = map[string]interface{}{
"every": "5m",
"target": "none",
}

patched, err := json.MarshalIndent(cfgJSON, "", " ")
if err != nil {
return fmt.Errorf("marshal patched config: %w", err)
}

applyPayload := map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "openclaw-config",
"namespace": namespace,
},
"data": map[string]string{
"openclaw.json": string(patched),
},
}
applyRaw, _ := json.Marshal(applyPayload)

applyCmd := exec.Command(kubectlBin,
"apply", "-f", "-",
"--server-side", "--field-manager=helm", "--force-conflicts")
applyCmd.Env = env
applyCmd.Stdin = bytes.NewReader(applyRaw)
var applyErr bytes.Buffer
applyCmd.Stderr = &applyErr
if err := applyCmd.Run(); err != nil {
return fmt.Errorf("patch heartbeat config: %w\n%s", err, applyErr.String())
}

// OpenClaw watches for ConfigMap file changes and hot-reloads config.
// No pod restart is needed: the running pod will detect the update within
// ~30-60s and apply [reload] config hot reload, switching the heartbeat
// interval to 5m immediately without losing the running pod or its state.
u.Success("Heartbeat config injected — OpenClaw hot reload will activate it (every 5m)")
return nil

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Mounting openclaw stuff like this as a config map means its read only to the runtime. This has ux problems because openclaw can't tune stuff for itself. If instead we do this with files on the host maybe they'll be mutable by the openclaw service?

Comment on lines +1 to +3
{
"workingDir": "/Users/bussyjd/Development/Obol_Workbench/obol-stack/.worktrees/autoresearch"
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

remove

Comment threadautoresearch.md
Comment on lines +174 to +177
### Session 1 (baseline → 61/61)

**Baseline: 44/57** — 13 failures across all flows.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should we move this to ./flows or somewhere other than top level if its not for users?

Comment threadautoresearch.sh
@@ -0,0 +1,48 @@
#!/bin/bash

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

subdirectory

@bussyjd

Copy link
Copy Markdown
ContributorAuthor

Closing — core CLI/Go changes were cherry-picked into feat/monetize-path (PR #288). The autoresearch shell scripts and top-level files flagged in @OisinKyne's review (hardcoded paths in autoresearch.config.json, autoresearch.sh/.md at root) were intentionally not carried over. Review feedback addressed by omission.

@OisinKyne
OisinKyne deleted the feat/autoresearch-flows branch April 23, 2026 19:26
Sign up for freeto 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.

fix: heartbeat timing bugs — reset on sell, missing activation, ConfigMap race

2 participants

@bussyjd@OisinKyne
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat: real user flow validation scripts + heartbeat timing fixes - #282

Closed
bussyjd wants to merge 1 commit into
mainfrom
feat/autoresearch-flows
Closed

feat: real user flow validation scripts + heartbeat timing fixes#282
bussyjd wants to merge 1 commit into
mainfrom
feat/autoresearch-flows

Conversation

@bussyjd

Copy link
Copy Markdown
Contributor

Summary

Introduces pi-autoresearch integration for automated real user flow validation, plus Go bug fixes and doc corrections discovered during the overnight run.

Commit 1: Flow scripts + autoresearch harness

10 bash flow scripts that exercise every documented user journey from docs/getting-started.md and docs/guides/monetize-inference.md:

flows/
├── lib.sh # Shared harness (step/pass/fail/poll, Anvil credentials)
├── flow-01-prerequisites.sh # Docker, Ollama, obol binary
├── flow-02-stack-init-up.sh # getting-started §1-2
├── flow-03-inference.sh # getting-started §3a-3d (LiteLLM, tool-calls)
├── flow-04-agent.sh # getting-started §4-5 (agent init, inference)
├── flow-05-network.sh # getting-started §6 (network add/remove)
├── flow-06-sell-setup.sh # monetize §1.1-1.4 (pricing, ServiceOffer, heartbeat)
├── flow-07-sell-verify.sh # monetize §1.5-1.7 (tunnel, routes, 402, metrics)
├── flow-08-buy.sh # monetize §2 (discovery, EIP-712 payment, settlement)
├── flow-09-lifecycle.sh # monetize §4 (stop, delete, cleanup)
└── flow-10-anvil-facilitator.sh # monetize §3 (Anvil fork, x402-rs facilitator)

All commands use the real obol binary — no go run, no direct kubectl, no pod IP access.

Commit 2: Bug fixes found by the flow scripts

Timing (closes#280):

  • ensureHeartbeatActive() — heartbeat was stuck at 30m default after obol agent init
  • patchHeartbeatAfterSync()SyncAgentBaseURL helmfile sync was resetting heartbeat config
  • readCurrentAgentBaseURL() — skip sync when tunnel URL unchanged (idempotency)

CLI validation:

  • validateRPCEndpoint() in obol network add — reject invalid URLs

Doc corrections:

  • getting-started §3c-3d: add LiteLLM Bearer auth header (was returning 401)
  • monetize §1.6: correct eRPC path to /rpc/evm/{chainId} (local-only, not via tunnel)
  • monetize §2.1: clarify /skill.md (always available) vs /.well-known (requires --register)

Discovery method

Overnight pi-autoresearch session: 44 → 133 steps passing across 50+ experiments. The agent autonomously ran flow scripts, diagnosed failures, fixed Go code and flow scripts, and re-validated — all while go build/go test acted as backpressure.

Test plan

  • go build ./... passes
  • go test ./internal/{agent,tunnel,network,openclaw,model}/ passes
  • pi-autoresearch: 133/133 steps, 50+ runs
  • Manual: bash flows/flow-01-prerequisites.sh through flow-09-lifecycle.sh on fresh cluster

@bussyjd
bussyjdforce-pushed the feat/autoresearch-flows branch from c040301 to e9683b1CompareMarch 19, 2026 10:03
Introduces pi-autoresearch integration with 10 flow scripts that exercise
every documented user journey from getting-started.md and monetize-inference.md.
Flow scripts (flows/):
- flow-01: prerequisites (Docker, Ollama, obol binary)
- flow-02: stack init + up + verify (getting-started §1-2)
- flow-03: LLM inference chain (getting-started §3a-3d)
- flow-04: agent init + inference (getting-started §4-5)
- flow-05: network add/remove (getting-started §6)
- flow-06: sell setup — pricing, ServiceOffer, heartbeat wait (monetize §1.1-1.4)
- flow-07: sell verify — tunnel, routes, 402, metrics (monetize §1.5-1.7)
- flow-08: buy — discovery, 402 parse, EIP-712 payment, settlement (monetize §2)
- flow-09: lifecycle — stop, delete, cleanup verification (monetize §4)
- flow-10: Anvil fork + x402-rs facilitator setup (monetize §3)
Bug fixes found by the flow scripts:
- ensureHeartbeatActive() — heartbeat stuck at 30m default after obol agent init
- patchHeartbeatAfterSync() — SyncAgentBaseURL helmfile sync was resetting heartbeat
- validateRPCEndpoint() — obol network add was accepting invalid URLs
- Doc corrections: LiteLLM auth, eRPC path, /skill.md vs /.well-known
Test account keys are derived at runtime from the Anvil mnemonic via
`cast wallet private-key` — no private keys in source.
Closes#280
@bussyjd
bussyjdforce-pushed the feat/autoresearch-flows branch from e9683b1 to e56ae6dCompareMarch 19, 2026 10:09
@bussyjd
bussyjd requested a review from OisinKyneMarch 19, 2026 10:20
@bussyjdbussyjd changed the title feat: autoresearch flow scripts + heartbeat/CLI/doc fixesfeat: real user flow validation scripts + heartbeat timing fixesMar 19, 2026

@OisinKyneOisinKyne left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

i'm a little unsure of the accuracy of the flows, at least they imply features i dont expect. I also think we probably need the heartbeat to be agent writable. I tried to get my obolclaw to modify key facts about itself and it was unable to (tuning down a 2 minute heartbeat was one). I don't know if giving it the auth to write to a config map is worth it, if instead we can get these files all mounted into the openclaw pv, and then obol cli just messes with the files on the host (e.g. how obol model sync can change the openclaw agent definition).

To that end, i might pull some of the fixes in separately to this pr, so as not to delay waiting for feedback. (the fact that the flow tests aren't in the user hot path makes me less worried about them being imperfect)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm a little confused by some of the changes in this file (but its a docs so nbd

Comment on lines +44 to +45
# Use qwen3.5:9b — it is configured in LiteLLM's model_list (FLOW_MODEL qwen3:0.6b
# is only registered in Ollama directly; the x402 sell/buy flows use it via that path)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

qwen3:0.6b, should we have a qwen3.5:?

out=$("$OBOL" kubectl exec -n llm deployment/litellm -c litellm -- \
python3 -c "
import urllib.request
r = urllib.request.urlopen('http://ollama.llm.svc.cluster.local:11434/api/tags', timeout=10)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we have internal ollama code? maybe we should internalise a llama.cpp pod

fail "LiteLLM inference failed — ${out:0:300}"
fi

# §3d: Tool-call passthrough

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Tool call at the litellm layer seems like it might confuse things

Comment on lines +147 to +227
func ensureHeartbeatActive(cfg *config.Config, u *ui.UI) error {
namespace := fmt.Sprintf("openclaw-%s", DefaultInstanceID)
kubectlBin := filepath.Join(cfg.BinDir, "kubectl")
kubeconfigPath := filepath.Join(cfg.ConfigDir, "kubeconfig.yaml")
env := append(os.Environ(), fmt.Sprintf("KUBECONFIG=%s", kubeconfigPath))

// Read current ConfigMap.
getCmd := exec.Command(kubectlBin,
"get", "configmap", "openclaw-config",
"-n", namespace,
"-o", "jsonpath={.data.openclaw\\.json}")
getCmd.Env = env
var outBuf bytes.Buffer
getCmd.Stdout = &outBuf
if err := getCmd.Run(); err != nil {
return fmt.Errorf("read openclaw-config: %w", err)
}

var cfgJSON map[string]interface{}
if err := json.Unmarshal(outBuf.Bytes(), &cfgJSON); err != nil {
return fmt.Errorf("parse openclaw.json: %w", err)
}

// Check whether heartbeat is already present.
agents, _ := cfgJSON["agents"].(map[string]interface{})
defaults, _ := agents["defaults"].(map[string]interface{})
_, alreadySet := defaults["heartbeat"]
if alreadySet {
u.Success("Heartbeat config already active")
return nil
}

// Inject heartbeat.
if agents == nil {
agents = map[string]interface{}{}
cfgJSON["agents"] = agents
}
if defaults == nil {
defaults = map[string]interface{}{}
agents["defaults"] = defaults
}
defaults["heartbeat"] = map[string]interface{}{
"every": "5m",
"target": "none",
}

patched, err := json.MarshalIndent(cfgJSON, "", " ")
if err != nil {
return fmt.Errorf("marshal patched config: %w", err)
}

applyPayload := map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "openclaw-config",
"namespace": namespace,
},
"data": map[string]string{
"openclaw.json": string(patched),
},
}
applyRaw, _ := json.Marshal(applyPayload)

applyCmd := exec.Command(kubectlBin,
"apply", "-f", "-",
"--server-side", "--field-manager=helm", "--force-conflicts")
applyCmd.Env = env
applyCmd.Stdin = bytes.NewReader(applyRaw)
var applyErr bytes.Buffer
applyCmd.Stderr = &applyErr
if err := applyCmd.Run(); err != nil {
return fmt.Errorf("patch heartbeat config: %w\n%s", err, applyErr.String())
}

// OpenClaw watches for ConfigMap file changes and hot-reloads config.
// No pod restart is needed: the running pod will detect the update within
// ~30-60s and apply [reload] config hot reload, switching the heartbeat
// interval to 5m immediately without losing the running pod or its state.
u.Success("Heartbeat config injected — OpenClaw hot reload will activate it (every 5m)")
return nil

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Mounting openclaw stuff like this as a config map means its read only to the runtime. This has ux problems because openclaw can't tune stuff for itself. If instead we do this with files on the host maybe they'll be mutable by the openclaw service?

Comment on lines +1 to +3
{
"workingDir": "/Users/bussyjd/Development/Obol_Workbench/obol-stack/.worktrees/autoresearch"
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

remove

Comment threadautoresearch.md
Comment on lines +174 to +177
### Session 1 (baseline → 61/61)

**Baseline: 44/57** — 13 failures across all flows.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should we move this to ./flows or somewhere other than top level if its not for users?

Comment threadautoresearch.sh
@@ -0,0 +1,48 @@
#!/bin/bash

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

subdirectory

@bussyjd

Copy link
Copy Markdown
ContributorAuthor

Closing — core CLI/Go changes were cherry-picked into feat/monetize-path (PR #288). The autoresearch shell scripts and top-level files flagged in @OisinKyne's review (hardcoded paths in autoresearch.config.json, autoresearch.sh/.md at root) were intentionally not carried over. Review feedback addressed by omission.

@OisinKyne
OisinKyne deleted the feat/autoresearch-flows branch April 23, 2026 19:26
Sign up for freeto 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.

fix: heartbeat timing bugs — reset on sell, missing activation, ConfigMap race

2 participants

@bussyjd@OisinKyne