Skip to content

feat(loop): structured planning system — plan tool, protected state, restart resume, Telegram/WebUI surfaces - #143

Open
jkyberneees wants to merge 3 commits into
mainfrom
feat/planning-system
Open

feat(loop): structured planning system — plan tool, protected state, restart resume, Telegram/WebUI surfaces#143
jkyberneees wants to merge 3 commits into
mainfrom
feat/planning-system

Conversation

@jkyberneees

Copy link
Copy Markdown
Contributor

Summary

Adds a structured planning layer to odek's ReAct loop and exposes it across surfaces. Design spec: docs/PLANNING.md (rewritten as reference documentation).

Core (33a8337)

  • One built-in plan tool (create/update/complete/get) maintaining engine-held PlanState; wholesale create is replanning
  • Plan state renders into a protected [Current plan: system message using the compaction-digest pattern: survives graduated trimming, survival trim, and process restarts (rides per-step session persistence; odek continue restores forward state via strict fail-closed transcript parsing)
  • Safe-classified in classifyToolCall — never prompts; untrusted-wrapped with audit ingest recording; overflow renders with an omission marker for live context but is rejected on resume; corrupt persisted plans dropped fail-closed in both directions
  • Config: planning.{enabled,max_steps,max_render_chars}, --no-planning, ODEK_PLANNING=false; project config may only lower caps; disabled ⇒ tool absent, zero plan logic

Surfaces (84e9b2e)

  • Additive odek.event/v1 types plan_created/plan_updated (counts + version only — never titles/notes; pinned by whole-stream marshal-and-scan test)
  • GET /api/sessions/{id}/plan — read-only, sibling auth/rate-limit path, GET-only enforced structurally
  • Telegram /plan_status — chat-scoped structured-plan reply; MarkdownV2-literal escaping of model-derived fields; rune-safe truncation; coexists with markdown-file /plan
  • WebUI plan panel — polls the endpoint while visible (runtime events relay to /api/events ring only; WS transport is a drop-in swap when it lands), textContent-only rendering pinned by an innerHTML tripwire test
  • Emoji: plan → 📋 in both renderers; vestigial todo arm retired

Coverage (5902828)

  • internal/loop/plan.go: 91.4% → 99.4% statements; every function ≥96.4%, most 100%. Remaining 2 blocks are provably unreachable defensive guards (documented)
  • cmd/odek plan surface functions at 100%; ui/js/plan.js at 98.35%

How loop performance improves

  • Decomposition survives trimming → late-run iterations stop re-deriving "what was I doing"
  • Done-work stays visible after its turns compact away → less redundant re-execution
  • Budget/iteration exhaustion + odek continue resume from an actionable queue instead of transcript archaeology (verified live: kill mid-run at exit 4, resume completed all remaining steps with zero rework)

Test plan

  • 24 new Go tests (loop lifecycle/trim/survival/resume/events/extract), 19 JS tests (panel, XSS inertness, polling lifecycle), 407 lines of parser coverage tests
  • go test ./... scoped suites green; -race clean on internal/loop; node --test 87/87
  • Live E2E against a real model: plan lifecycle (v1→v7, collapse-on-done, single persisted message) and kill-and-resume (exit 4 → continue → completion) both verified
  • Security regression bar extended: Safe classification, untrusted wrapping, project-config clamp pinned in security_report_validation_test.go

Out of scope (roadmap in docs/PLANNING.md)

Plan-aware stall-recovery suffix, blocked-step trigger + plan_blocked signal, exhaustion remaining-steps blocks, WS event relay for live panel updates.

…ate, restart resume

One built-in plan tool (create/update/complete/get) maintains engine-held
PlanState rendered into a protected [Current plan: system message using the
compaction-digest pattern: upsert-in-place, headLen/trimToSurvival protection,
restart resume via strict fail-closed transcript parsing. Safe-classified
(never prompts), untrusted-wrapped with audit ingest recording, config-gated
(planning.{enabled,max_steps,max_render_chars}, --no-planning, ODEK_PLANNING;
project config may only lower caps). Overflow renders with an omission marker
for live context but is rejected on resume; corrupt persisted plans are
dropped in both directions. Reference doc: docs/PLANNING.md.

Reviewed: 7 findings (2 HIGH) fixed and mutation-verified; race-clean; 30+
new tests across loop/config/cmd suites.
…ew, /plan_status, WebUI panel

Observability: additive odek.event/v1 types plan_created/plan_updated emitted
once per effective version-bumping mutation via PlanStore.SetOnChange;
payloads carry counts + version only (never titles/notes). Shared exported
extractor loop.ExtractPlan (newest-parseable-wins, fail-closed) backs both
read-only surfaces:

- GET /api/sessions/{id}/plan — sibling auth/rate-limit path, GET-only,
  found:false when absent, 404 unknown session.
- Telegram /plan_status — chat-scoped structured-plan reply (glyphs per
  status, MarkdownV2-literal escaping of model-derived fields, rune-safe
  truncation, 3800-char bound); coexists with markdown-file /plan.
- WebUI plan panel — drawer tab polling the endpoint every 5s while visible
  (runtime events relay to the /api/events ring only; WS transport is a
  drop-in swap when it lands), textContent-only rendering pinned by an
  innerHTML tripwire test, session-switch/visibility lifecycle covered.
- Emoji: plan → 📋 in both renderers; vestigial todo arm retired.

Docs: PLANNING.md observability + surface sections rewritten as reference,
WEBUI/TELEGRAM/EXTENSIONS/API/CHEATSHEET updated.

Reviewed: APPROVE (0 critical/high); reviewer LOWs fixed in-commit
(MarkdownV2 literal escaping, rune-boundary truncation, cap-drift doc note).
Tests: 24 new Go tests + 19 JS tests; race-clean.
Table-driven behavioral tests for parser internals: header variants,
omission-marker semantics (incl. [+0 …] not being an overflow marker),
wrapper unwrap rejections and newline artifacts, step-line rejections
with exact error strings, number-parsing boundaries, degenerate
NewPlanStore caps falling back to defaults, nil-store tool Call guard,
blocked-count event payload, UTF-8-boundary render hard-cut.
Two remaining uncovered blocks are provably unreachable defensive guards.
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
odek 5902828 Commit Preview URL

Branch Preview URL
Aug 24 2026, 08:03 PM

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant