Skip to content

v0.6: jsonl response path + session memory + global CLI + cron hardening#1

Merged
breaktheready merged 12 commits into
mainfrom
feat/macos-support
Apr 23, 2026
Merged

v0.6: jsonl response path + session memory + global CLI + cron hardening#1
breaktheready merged 12 commits into
mainfrom
feat/macos-support

Conversation

@breaktheready

@breaktheready breaktheready commented Apr 19, 2026

Copy link
Copy Markdown
Owner

Summary

This PR consolidates the feat/macos-support line of work into main. It began
as a macOS port of the bridge (start.sh, LaunchAgent proxy) and grew into a
reliability + UX overhaul that benefits all platforms.

Commits (oldest → newest)

  1. [fix] cron day-of-week — APScheduler was interpreting 1-5 as Tue-Sat; corrected to Unix Mon-Fri semantics.
  2. [macOS] platform-aware proxy recovery + start.sh launcher + MAC-OPS — Docker on Mac can't read keychain; LaunchAgent instead.
  3. [feat] global liteclaw CLI + setup.sh enhancementsbin/liteclaw + symlink install + .tmux.conf defaults.
  4. [feat] start.sh --session-id <uuid> — deterministic session pinning.
  5. [feat] jsonl response path + session memory + boot notify + cron hardening — the big one (see below).
  6. [docs] README + README_KO v0.6
  7. [feat] /tell-summarizer — runtime control over summarizer system prompt.
  8. [chore] community hygiene — badges, CI, issue templates, CONTRIBUTING.md.

Headline changes

Response source: tmux pane → session JSONL

Instead of scraping the tmux pane (brittle: ANSI chrome leaks, scrollback truncation, summarizer over-compression), the daemon now reads Claude Code's own session log at ~/.claude/projects/<encoded-cwd>/<session-id>.jsonl and extracts clean text blocks. Completion via stop_reason=end_turn. Automatic fallback to pane-scrape on jsonl failure.

OpenClaw-style memory layout (~/.liteclaw/)

Per-day transcripts, daily markdown digests (LLM-compacted), rolling strategic summary, and a startup primer assembled from recent + strategic context. Legacy ~/.liteclaw-history.jsonl auto-migrates. Transcripts use an integer sid alias (index into sessions.json.history[]) — ~70% shorter per row than embedding full UUIDs.

Cron hardening

  • Trust-prompt auto-accept
  • Fixed is_idle_prompt false-positive on UI chrome (removed · / * from spinner set, anchored pattern to line start)
  • Busy-wait 120s → 300s with a has_prompt+stable-pane fallback
  • Failures captured (pane snapshot + job config) to ~/.liteclaw/cron-error-capture.md

Telegram UX

  • BOOT_NOTIFY: one-shot "ready" ping after init with 5-min rate limit
  • Mid-poll status edits gated behind SHOW_POLLING_STATUS (off by default) — these were the source of "Thinking (Crystalizing)…" leaks
  • _followup_edit skipped when jsonl returned a complete turn — prevents delivered message being overwritten by stale status edit
  • /tell-summarizer <instruction> for runtime summarizer tuning from Telegram

Global CLI

liteclaw start|stop|restart|status|logs|attach works from any directory (setup.sh symlinks to ~/.local/bin/liteclaw). Underlying start.sh still works.

Test plan

  • bash -n setup.sh start.sh bin/liteclaw — clean
  • python3 -c "import ast; ast.parse(open('liteclaw.py').read())" — clean
  • Manual: fresh liteclaw start, Telegram roundtrip delivers via jsonl
  • Manual: liteclaw stop / status / logs / attach
  • Manual: legacy history migrated on first boot
  • Manual: _compact_day produces daily memory/*.md
  • Manual: cron survives trust prompt and launches fresh Claude successfully
  • CI runs green (first run triggered when this PR updates)

User-visible diff on Telegram

Before: long answers truncated mid-chunk, "Thinking (Crystalizing)…" sometimes leaked, placeholder occasionally overwritten by short status edit after the real answer arrived.

After: one placeholder ("⏳ 작업 중…") edit-in-place to the full final text, no mid-poll noise, no summarizer over-compression when jsonl path succeeds.

Config migration

New .env keys (all optional, defaults preserve prior behavior):

  • BOOT_NOTIFY (default 1)
  • LITECLAW_DIR (default ~/.liteclaw)
  • CLAUDE_CWD (default $HOME)
  • PRIMER_RECENT_TURNS (default 20)
  • USE_JSONL_RESPONSE (default 1)
  • SHOW_POLLING_STATUS (default 0)

🤖 Generated with Claude Code

breaktheready and others added 8 commits April 19, 2026 20:32
APScheduler 3.x's CronTrigger.from_crontab() interprets numeric day-of-week
fields with APScheduler's internal index (0=Mon, 6=Sun) instead of Unix cron
(0/7=Sun, 1=Mon, ..., 6=Sat). For users with weekday-only crons like
"0 20 * * 1-5", jobs would silently fire Tue-Sat instead of Mon-Fri.

Add a build_cron_trigger() helper that translates numeric DoW tokens to
APScheduler name form (mon/tue/...) before constructing CronTrigger, then
route the four call sites in LiteClaw through it.

Discovered when a Mon-Fri 15:20 market-coach cron fired on a Saturday.
…uide

Adds first-class macOS support:

- liteclaw.py: _recover_proxy() now branches on sys.platform. On Darwin it
  invokes `launchctl kickstart -k gui/<uid>/com.claude-max-api-proxy` to bounce
  the LaunchAgent (Docker on macOS cannot read the keychain where the Claude
  CLI stores OAuth tokens). Linux behavior is unchanged.

- start.sh: idempotent launcher that brings up tmux 'claude' (Claude Code
  CLI) first, waits for the prompt (auto-accepting the "trust this folder"
  dialog), then starts LiteClaw in tmux 'liteclaw'. Use --attach to land in
  the claude pane.

- MAC-OPS.md: setup, management, and troubleshooting for the LaunchAgent
  approach, including the IPv4/IPv6 ghost-container hazard.

- CLAUDE.md: cross-references MAC-OPS for the macOS auto-recovery and
  proxy infrastructure paths.

- README.md: new Platform Support table making clear that Linux/macOS are
  supported natively, while Windows requires WSL (LiteClaw depends on tmux).
  Adds a "One-command launch" subsection pointing at start.sh.
- New bin/liteclaw dispatcher with subcommands:
  start [--attach] / stop / restart / status / logs / attach / help.
  Resolves repo root via script location or $LITECLAW_HOME.
- setup.sh installs a symlink at ~/.local/bin/liteclaw so the
  dispatcher is reachable from any directory.
- setup.sh also plants sensible ~/.tmux.conf defaults when missing
  (mouse on, history-limit 50000) so Claude Code panes are
  scroll-wheel friendly out of the box.
- .env.example: new flags BOOT_NOTIFY, LITECLAW_DIR, CLAUDE_CWD,
  PRIMER_RECENT_TURNS documented for the OpenClaw-style memory
  layout and boot-ready Telegram ping.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces --continue with --session-id <uuid> using a stable UUID
stored in ~/.liteclaw/sessions.json.liteclaw_session_id. Benefits:

- Deterministic resume — other Claude Code windows in the same cwd
  no longer steal which session gets picked up.
- Migration-friendly — if sessions.json has no UUID but the target
  cwd already has exactly one Claude Code session jsonl, start.sh
  adopts that UUID so existing conversations carry over.
- Allocates fresh UUID via `uuidgen` otherwise.

CLAUDE_CWD env (default $HOME) pins the working directory so the
session log path under ~/.claude/projects/<encoded-cwd>/ is stable
regardless of where the user runs `liteclaw start` from.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ening

This is the big one — a set of related changes to (1) stop scraping
tmux for Claude's response, (2) give LiteClaw an OpenClaw-style
persistent memory, and (3) make crons survive more failure modes.

Response source: tmux pane → session JSONL
  - _jsonl_path / _record_jsonl_offset / _tail_jsonl_since_offset /
    _poll_response_via_jsonl read Claude Code's own session log at
    ~/.claude/projects/<encoded-cwd>/<session-id>.jsonl and extract
    only `text` blocks from assistant messages in the new range.
  - Completion detected by stop_reason=end_turn|stop_sequence|
    max_tokens. `tool_use` stop_reason means mid-turn; keep waiting.
  - File-size-based stable safety net (15 s of no jsonl growth with
    some text already captured → assume flushed). Avoids previous
    bug where long tool_use chains tripped a text-stable timeout.
  - handle_message replaces pane-derived response with jsonl text
    when jsonl length ≥ 50% of pane length (guards against returning
    a preamble-only stub if tool chain was still running).
  - When jsonl delivers a complete turn, _skip_summarizer_once fires
    so the summarizer doesn't re-compress clean text, and
    _jsonl_delivered_complete suppresses _followup_edit (which was
    overwriting delivered messages with "Working… (Ns)" status).
  - Mid-poll status edits gated behind SHOW_POLLING_STATUS (off by
    default) — these were the source of "Thinking (Crystalizing)…"
    leaks into Telegram.
  - Fallback to pane-scrape preserved on any jsonl failure.

Memory layout (OpenClaw-inspired):
  ~/.liteclaw/
    transcripts/YYYY-MM-DD.jsonl   per-day append-only, schema with
                                   sid:<int> alias (index into
                                   sessions.json.history[]), ~70%
                                   shorter than embedding full UUID
    memory/YYYY-MM-DD.md           LLM-compacted daily digest
    memory/strategic.md            optional rolling summary
    sessions.json                  {liteclaw_session_id, history[]}
    primer.md                      assembled at boot (strategic +
                                   recent N turns)
  - _migrate_legacy_history splits the old single-file jsonl into
    per-day files on first boot.
  - _compact_day calls the summarizer (tier-1 proxy, openai-compat)
    and writes memory/YYYY-MM-DD.md; idempotent (skip if already
    populated).
  - _build_primer composes primer.md; falls back to recent daily
    memory markdowns when strategic.md is absent.
  - _detect_resume_state also maintains sessions.json.history[]
    append-only (new UUIDs pushed, prior `ended_at` stamped).

Boot notify + rate limit:
  - _send_boot_ready sends a one-shot "🚀 LiteClaw ready" Telegram
    message after init, reporting host/target/history/resume/primer.
  - Uses sync httpx.Client because it fires before run_polling takes
    the event loop.
  - 5-minute rate limit via .last_boot_at file so dev/debug restarts
    don't spam. Bookkeeping (migrate/compact/primer/history) still
    runs even when the ping is suppressed.

Cron hardening:
  - _run_cron_job auto-accepts Claude Code's "Yes, I trust" dialog
    on first launch in unattended cwds.
  - Busy-wait extended 120 s → 300 s with a has_prompt+stable-pane
    fallback — addresses the "Session busy for 120s" false fail that
    fired on cold Claude launches with chrome glyphs.
  - _log_cron_error appends a forensic entry (ts, job config, pane
    snapshot) to ~/.liteclaw/cron-error-capture.md on every failure.
  - Failure Telegram alert references that file.

is_idle_prompt correctness:
  - Removed `·` and `*` from the spinner character set (they are
    separators in Claude Code's banner, not real frames) and
    anchored _ACTIVITY_PATTERNS to line start. Without this, the
    banner "· Claude Max" line matched as "activity" forever and
    is_idle_prompt never returned True.

/recall session:
  - Session-scoped recall. `/recall session` restricts to current
    session; `/recall session <uuid>` targets a specific past one.
  - Filter accepts both legacy full-UUID session_id entries and new
    sid:<int> aliases for back-compat.

Misc:
  - USE_JSONL_RESPONSE / SHOW_POLLING_STATUS / BOOT_NOTIFY /
    LITECLAW_DIR / CLAUDE_CWD / PRIMER_RECENT_TURNS env flags.
  - Summarizer prompt gains a PRESERVE-AS-IS block for numbered /
    lettered option lists so "B / C / D 선택" messages don't lose
    their option bodies in the tmux-pane path.
  - Placeholder text changed from "📤 Sent. Waiting for response..."
    to "⏳ 작업 중…".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- New "What's new in v0.6 (April 2026)" section (EN + KO) covering:
  global CLI, --session-id pinning, jsonl response path, OpenClaw-
  style memory layout, boot-ready ping, cron hardening,
  /recall session, quieter Telegram UX.
- Configuration table gains BOOT_NOTIFY, LITECLAW_DIR, CLAUDE_CWD,
  PRIMER_RECENT_TURNS, USE_JSONL_RESPONSE, SHOW_POLLING_STATUS.
- Quick Start documents `liteclaw start|stop|...` as the preferred
  entry point (global CLI via ~/.local/bin/liteclaw symlink).
- Notes that the underlying start.sh and manual flow still work.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New command to append user-provided instructions to the summarizer's
system prompt at runtime, without code edits. Useful when the user
wants to tune summarization behavior ("never drop numbered option
lists", "always preserve code blocks verbatim", etc.) from Telegram.

Commands:
  /tell-summarizer <instruction>   append to extra prompt (in-memory)
  /tell-summarizer show            display current extras
  /tell-summarizer clear           wipe extras
  /tellsum                         short alias

Persistence: in-memory only. Resets on daemon restart. This is
intentional — runtime tuning should not quietly leak across restarts.

Integration point: `_summarize` appends
`self._summarizer_extra_prompt` to `SUMMARIZE_PROMPT` under a
"USER-PROVIDED RUNTIME INSTRUCTIONS" header before sending to the
tier-1 proxy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- README.md / README_KO.md: shields.io badges (License, Python,
  Platform, CI, GitHub stars) at the top.
- .github/workflows/ci.yml: minimal but meaningful checks
  - liteclaw.py AST parse
  - bash -n on setup.sh / start.sh / bin/liteclaw
  - requirements.txt install + import check (httpx, telegram,
    dotenv, apscheduler)
  - advisory ruff lint (continue-on-error until codebase catches up)
- .github/ISSUE_TEMPLATE/bug_report.md and feature_request.md with
  LiteClaw-specific prompts (env versions, cron-error-capture.md
  reference, scope checkboxes).
- CONTRIBUTING.md: ground rules (single-file core, no new API key,
  fallbacks non-negotiable), project structure, commit style,
  release flow. Calls out that Telegram-side UX regressions are
  easy to miss in code review and asks PRs to attach a
  before/after transcript when relevant.

No runtime behavior change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@breaktheready breaktheready changed the title macOS support + cron day-of-week fix v0.6: jsonl response path + session memory + global CLI + cron hardening Apr 22, 2026
breaktheready and others added 4 commits April 23, 2026 21:23
Adds two layers of protection so user replies don't get silently lost
when the daemon is restarted (intentional or crash) while a poll task
is in flight:

1. Persistent pending replies (~/.liteclaw/pending_replies.json)
   - Every user message with a placeholder sent but no reply yet is
     recorded with: user_msg_id, placeholder_msg_id, chat_id, jsonl
     offset, sent timestamp, target.
   - On daemon boot, _resume_pending_on_boot (registered as
     Application.post_init) walks the file:
       * if the session jsonl now has a complete turn → edit the
         placeholder in-place with the answer
       * else if the entry is older than PENDING_MAX_AGE_SEC (default
         900 s) → mark the placeholder as "재기동으로 유실됨"
       * else → spawn a background polling task that completes the
         delivery later
   - Survives crashes / SIGKILL / power loss because state lives on disk.

2. Graceful shutdown
   - Application.run_polling now passes
     stop_signals=(SIGINT, SIGTERM) explicitly so pkill / launchctl
     bootout trigger PTB's in-flight handler drain instead of cutting
     mid-update.

Behavioral changes:
  - handle_message now records pending right after sending the
    placeholder and clears it after _deliver_response returns.
  - All bookkeeping is best-effort; failures log a warning and never
    block the user-facing path.

New env: PENDING_MAX_AGE_SEC (default 900) — entries older than this
on boot are abandoned with a "lost" notice rather than re-polled.

Also adds CHANGELOG.md covering this work plus the v0.6.1 fixes done
in the same session (cron path fix, market-coach timeout bumps,
launchd TZ fix, okl-ear path + buffering fix, knowledge.json prompt
injection, /tell-summarizer command, battery morning check cron).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Brings the project CHANGELOG.md up to date. v0.6.0 summarizes the
work shipped in PR #1 (jsonl response path, session memory, global
CLI, cron hardening); v0.6.1 captures the follow-up work from the
2026-04-23 session:

- daemon-restart resilience (persistent pending_replies + graceful
  shutdown)
- /tell-summarizer (/tellsum) runtime control over summarizer prompt
- battery-morning-check cron for 삼성SDI / 에코프로 (parallel to the
  existing taihan-morning-check)
- community hygiene set (CONTRIBUTING, CI workflow, issue templates,
  README badges)
- path fixes: ~/.openclaw/... → ~/projects/openclaw-archive/... on
  5 cron jobs
- timeout bumps: market-coach-* 300→600 s, taihan-morning-check
  900→1200 s
- schedule shift: market-coach-preopen 08:03 → 07:45 KST (before NXT)
- macOS launchd TZ handling: explicit Asia/Seoul via setenv-tz
  LaunchAgent + LimitLoadToSessionType broadening so agents fire
  during display sleep
- okl-ear-report: hardcoded Linux .env path → Path.home(), `-u` on
  Python for real-time log flush, knowledge.json injected into LLM
  prompt so participant names are resolved (불명확 36→24 measured)
- SUMMARIZE_PROMPT preserves numbered/lettered option lists verbatim

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous CHANGELOG v0.6.1 entry leaked details about my
personal Mac mini setup (okl-ear / autotrade / LaunchAgent TZ /
knowledge.json / cron config references) that are not part of
the LiteClaw bridge itself. Those belong in DEVNOTES.md (which
is gitignored).

Keep in CHANGELOG (actual LiteClaw code/doc changes):
- daemon-restart resilience (pending_replies.json + post_init
  resume + explicit stop_signals)
- /tell-summarizer (/tellsum) runtime summarizer prompt control
- community hygiene set (CONTRIBUTING, CI workflow, issue
  templates, README badges)
- SUMMARIZE_PROMPT PRESERVE-AS-IS block for option lists

Removed from CHANGELOG (moved to DEVNOTES.md privately):
- .cron_jobs.json path/timeout/schedule tweaks (my personal crons)
- LaunchAgent TZ + LimitLoadToSessionType fixes (my OS-level config)
- okl-ear-report path + -u + knowledge.json injection (separate project)
- battery-morning-check cron specifics (my workflow)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a new `type: "subprocess"` variant to `.cron_jobs.json`. When set,
`_run_cron_job` dispatches to `_run_subprocess_cron`, which:

- Runs `job["command"]` via `asyncio.create_subprocess_shell` under
  `job["project"]` cwd, capturing combined stdout+stderr.
- Enforces `job["timeout"]` (default 1200 s) by killing the process.
- Persists `last_run` / `last_status` like regular cron jobs.
- Optionally sends a compact Telegram summary (tail N lines, controlled
  by `notify_telegram` and `notify_tail_lines`).
- On non-zero exit or timeout, writes to ~/.liteclaw/cron-error-capture.md
  and fires a ❌ Telegram alert.

Motivation: on macOS, LaunchAgents with StartCalendarInterval are
gated by `com.apple.UserEventAgent-Aqua`. When the GUI session isn't
active (display lock, fast-user-switch, brief inactivity) the job
silently skips. LaunchAgents are the wrong tool for reliability on a
headless-ish workstation. LiteClaw's APScheduler runs inside the
always-on daemon so the job fires regardless of GUI state.

The historical `type: "claude"` path (tmux + Claude Code LLM cron)
remains the default and is untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@breaktheready
breaktheready merged commit 199c8e4 into main Apr 23, 2026
4 checks passed
@breaktheready
breaktheready deleted the feat/macos-support branch April 23, 2026 14:00
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