Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a838f46
feat(convo): session-level user_actor override in DeriveConfig
akesling Jun 30, 2026
b3333b9
docs(openclaw): implementation plan for toolpath-openclaw crate
akesling Jun 30, 2026
10b90b8
feat(openclaw): crate skeleton (Cargo, error types, module stubs)
akesling Jun 30, 2026
e65c2b6
feat(openclaw): v3 JSONL schema types + serde round-trip
akesling Jun 30, 2026
243a34a
feat(openclaw): path + session-key resolution
akesling Jun 30, 2026
5cf397b
feat(openclaw): session reader + tree assembly + listing/metadata
akesling Jun 30, 2026
507f03a
feat(openclaw): session_to_view + tool/file/usage mapping + channel a…
akesling Jun 30, 2026
d7382d0
feat(openclaw): derive_path/derive_graph with channel-aware actor + meta
akesling Jun 30, 2026
a9538ab
feat(openclaw): OpenClawConvo manager + ConversationProvider + derive…
akesling Jun 30, 2026
a311698
feat(openclaw): projector + sessions.json inception + full round-trip…
akesling Jun 30, 2026
0aa9881
feat(cli): path p import openclaw (--agent/--session/--all/--base + p…
akesling Jun 30, 2026
21c3823
feat(cli): path p list/show openclaw
akesling Jun 30, 2026
36946bd
feat(openclaw): wire toolpath-openclaw CLI (share/resume/export) + wo…
akesling Jul 1, 2026
914b1b6
chore(scripts): add openclaw-docker.sh to launch OpenClaw in Docker f…
akesling Jul 7, 2026
1c33b32
fix(scripts): openclaw-docker onboarding --skip-health + embedded --l…
akesling Jul 7, 2026
0eba9aa
feat(cli): inception+ resume for openclaw (adopt-if-running, else exe…
akesling Jul 7, 2026
43d4dad
fix(openclaw): sessions.json upsert merges raw JSON instead of clobbe…
akesling Jul 8, 2026
4850eb6
feat(openclaw): real-session fixtures + spawn/subagent classification…
akesling Jul 8, 2026
f9ed14b
test(cli): openclaw in the cross-harness matrix (all cells pass on th…
akesling Jul 8, 2026
01a344b
docs(openclaw): upgrade format reference to observed + record real-da…
akesling Jul 8, 2026
5141b85
feat(convo): typed round-trip fidelity fields (signatures, response_m…
akesling Jul 8, 2026
fe335a2
feat(openclaw): byte-faithful round-trips + cumulative-usage aggregat…
akesling Jul 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,57 @@

All notable changes to the Toolpath workspace are documented here.

## toolpath-convo 0.13.0: typed round-trip fidelity fields — 2026-07-08

Adds typed IR homes for cross-provider replay-fidelity data that providers
previously had to drop (no provider-namespaced extras exist on the IR by
design): `Turn.thinking_signature` / `Turn.text_signature` (opaque
reasoning/text replay signatures — Anthropic thinking `signature`,
OpenClaw `thinkingSignature`/`textSignature`), `Turn.response_model`
(concrete served model under alias/auto routing), `Turn.marker`
(`ConversationMarker::Compaction` / `BranchSummary` with
`first_kept_id`/`tokens_before`/file lists/`from_hook` so structural
boundaries survive projection), and
`ToolInvocation.thought_signature`/`execution_mode`. All optional,
serde-defaulted, written/read by the shared `derive_path` /
`extract_conversation` (new `conversation.append` keys are valid under the
permissive kind v1.1.0 schema; a future kind revision will document them).

`toolpath-openclaw` uses all of them: signatures, `responseId`
(→ `Turn.group_id`, the per-response accounting id), `responseModel`,
and compaction/branch markers now survive session → Path → session
round-trips byte-faithfully; `totalTokens` re-emits with the observed
`input+output+cacheRead+cacheWrite` convention. Also fixes a real
double-counting bug found in captured data: OpenClaw's final assembled
reply after a `sessions_yield` carries run-cumulative usage with no
`responseId`; the provider now detects that aggregate row (exact
field-wise sum match, native files only) and refuses to stamp it onto a
step, keeping session totals single-counted.

## toolpath-openclaw 0.1.0: new provider (derive + inception) — 2026-07-08

New crate `toolpath-openclaw`: reads OpenClaw's multi-channel agent-session
JSONL (`~/.openclaw/agents/<id>/sessions/`, format v3), derives `Path`
documents with channel-aware `human:<channel>/<peerId>` actors recovered
from the `sessions.json` routing key, and projects/incepts a `Path` back
onto disk (transcript + routing entry) that a **running** OpenClaw gateway
adopts without restart — verified live. Wired through the `path` CLI
(`p import/list/show/export openclaw`, `share`, and "inception+" `resume`
that skips exec when a gateway is already listening). Validated against
real sessions captured from the official Docker image (v2026.6.11),
committed as `test-fixtures/openclaw/` and wired into the cross-harness
matrix (all cells pass). File changes are structural only — OpenClaw
stores no diffs.

## toolpath-convo 0.12.0: session-level `user_actor` override — 2026-06-30

Adds `DeriveConfig.user_actor: Option<String>` so a provider can set a
session-level actor string for user turns (e.g. a channel-aware
`human:whatsapp/<peerId>`) instead of the default `human:user`. Additive
and backward-compatible — existing callers using `..Default::default()`
are unaffected. Enables the multi-channel human identity in the new
`toolpath-openclaw` provider.

## Token usage: once per message, with per-step attribution + kind v1.1.0 — 2026-06-17

Fixes token over-counting in derived documents (~3× output-token
Expand Down
15 changes: 10 additions & 5 deletions CLAUDE.md

Large diffs are not rendered by default.

17 changes: 16 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ members = [
"crates/toolpath-codex",
"crates/toolpath-opencode",
"crates/toolpath-cursor",
"crates/toolpath-pi",
"crates/toolpath-openclaw",
"crates/toolpath-dot",
"crates/toolpath-md",
"crates/toolpath-pi",
"crates/pathbase-client",
"crates/path-cli",
]
Expand All @@ -24,7 +25,7 @@ license = "Apache-2.0"

[workspace.dependencies]
toolpath = { version = "0.7.0", path = "crates/toolpath" }
toolpath-convo = { version = "0.11.0", path = "crates/toolpath-convo" }
toolpath-convo = { version = "0.13.0", path = "crates/toolpath-convo" }
toolpath-git = { version = "0.6.0", path = "crates/toolpath-git" }
toolpath-claude = { version = "0.12.0", path = "crates/toolpath-claude", default-features = false }
toolpath-gemini = { version = "0.6.0", path = "crates/toolpath-gemini", default-features = false }
Expand All @@ -35,6 +36,7 @@ toolpath-github = { version = "0.6.0", path = "crates/toolpath-github" }
toolpath-dot = { version = "0.5.0", path = "crates/toolpath-dot" }
toolpath-md = { version = "0.7.0", path = "crates/toolpath-md" }
toolpath-pi = { version = "0.6.0", path = "crates/toolpath-pi" }
toolpath-openclaw = { version = "0.1.0", path = "crates/toolpath-openclaw" }
path-cli = { version = "0.14.0", path = "crates/path-cli" }
pathbase-client = { version = "0.2.0", path = "crates/pathbase-client" }

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ crates/
toolpath-opencode/ Derive from opencode SQLite databases
toolpath-cursor/ Derive from Cursor (IDE) state.vscdb bubble store
toolpath-pi/ Derive from Pi (pi.dev) agent sessions
toolpath-openclaw/ Derive from OpenClaw agent-session logs
toolpath-dot/ Graphviz DOT visualization
toolpath-md/ Markdown rendering for LLM consumption
pathbase-client/ Progenitor-derived typed client for the Pathbase HTTP API
Expand Down
2 changes: 2 additions & 0 deletions crates/path-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ toolpath-codex = { workspace = true }
toolpath-opencode = { workspace = true }
toolpath-cursor = { workspace = true }
toolpath-pi = { workspace = true }
toolpath-openclaw = { workspace = true }
toolpath-convo = { workspace = true }
toolpath-github = { workspace = true }
pathbase-client = { workspace = true }
Expand All @@ -69,6 +70,7 @@ toolpath-claude = { workspace = true }
toolpath-gemini = { workspace = true }
toolpath-codex = { workspace = true }
toolpath-pi = { workspace = true }
toolpath-openclaw = { workspace = true }

[features]
default = ["embedded-picker"]
Expand Down
112 changes: 112 additions & 0 deletions crates/path-cli/src/cmd_export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,24 @@ pub enum ExportTarget {
#[arg(short, long, conflicts_with = "project")]
output: Option<PathBuf>,
},
/// Project ("incept") a toolpath document into an OpenClaw session
Openclaw {
/// Input: cache id (e.g. `claude-abc`) or path to a toolpath JSON file
#[arg(short, long)]
input: String,

/// Working directory to record in the session header. With this flag,
/// the session is incepted into `~/.openclaw/agents/<agent>/sessions/`
/// (plus a `sessions.json` routing entry) so a running OpenClaw
/// instance can pick it up. Defaults to cwd when neither --project nor
/// --output is given.
#[arg(short, long)]
project: Option<PathBuf>,

/// Output JSONL to this file. Mutually exclusive with --project.
#[arg(short, long, conflicts_with = "project")]
output: Option<PathBuf>,
},
/// Project a toolpath document into a Codex CLI session
Codex {
/// Input: cache id (e.g. `claude-abc`) or path to a toolpath JSON file
Expand Down Expand Up @@ -213,6 +231,11 @@ pub fn run(target: ExportTarget) -> Result<()> {
project,
output,
} => run_pi(input, project, output),
ExportTarget::Openclaw {
input,
project,
output,
} => run_openclaw(input, project, output),
ExportTarget::Codex {
input,
project,
Expand Down Expand Up @@ -375,6 +398,95 @@ pub(crate) fn project_pi(
Ok(session.header.id)
}

/// Incept a Path into the real OpenClaw state dir (`~/.openclaw`), recording
/// `cwd` in the header. Returns the session id. Used by `path resume`.
pub(crate) fn project_openclaw(
path: &toolpath::v1::Path,
cwd: &std::path::Path,
) -> Result<String> {
let (projector, session) = build_openclaw(path, &cwd.to_string_lossy())?;
let state_dir = toolpath_openclaw::PathResolver::new()
.state_dir()
.to_path_buf();
projector
.write_session(&session, &state_dir)
.map_err(|e| anyhow::anyhow!("OpenClaw inception failed: {}", e))?;
Ok(session.header.id)
}

/// Build an OpenClaw projector (channel/peer/agent recovered from
/// `meta.extra["openclaw"]`) plus the projected session.
fn build_openclaw(
path: &toolpath::v1::Path,
cwd: &str,
) -> Result<(
toolpath_openclaw::project::OpenClawProjector,
toolpath_openclaw::OpenClawSession,
)> {
use toolpath_convo::ConversationProjector;
let view = toolpath_convo::extract_conversation(path);
let mut projector =
toolpath_openclaw::project::OpenClawProjector::new().with_cwd(cwd.to_string());
if let Some(extra) = path.meta.as_ref().and_then(|m| m.extra.get("openclaw")) {
projector = projector.with_meta_extra(extra);
}
let session = projector
.project(&view)
.map_err(|e| anyhow::anyhow!("Projection failed: {}", e))?;
if session.header.id.is_empty() {
anyhow::bail!("Projected session has no id");
}
Ok((projector, session))
}

fn openclaw_session_to_jsonl(session: &toolpath_openclaw::OpenClawSession) -> Result<String> {
let mut out = String::new();
for entry in &session.entries {
out.push_str(&serde_json::to_string(entry)?);
out.push('\n');
}
Ok(out)
}

fn run_openclaw(input: String, project: Option<PathBuf>, output: Option<PathBuf>) -> Result<()> {
#[cfg(target_os = "emscripten")]
{
let _ = (input, project, output);
anyhow::bail!("'path p export openclaw' requires a native environment");
}
#[cfg(not(target_os = "emscripten"))]
{
let path = load_path_doc(&input)?;
match (project, output) {
(Some(project_dir), None) => {
let id = project_openclaw(&path, &project_dir)?;
let state = toolpath_openclaw::PathResolver::new()
.state_dir()
.to_path_buf();
eprintln!("Incepted OpenClaw session {id} into {}", state.display());
eprintln!();
eprintln!("A running OpenClaw instance routes it via sessions.json.");
}
(None, Some(out_path)) => {
let cwd = std::env::current_dir()
.map(|p| p.to_string_lossy().into_owned())
.unwrap_or_else(|_| "/".to_string());
let (_projector, session) = build_openclaw(&path, &cwd)?;
let jsonl = openclaw_session_to_jsonl(&session)?;
std::fs::write(&out_path, &jsonl)
.with_context(|| format!("write {}", out_path.display()))?;
eprintln!("Wrote {} bytes to {}", jsonl.len(), out_path.display());
}
(None, None) => {
let (_projector, session) = build_openclaw(&path, "/")?;
print!("{}", openclaw_session_to_jsonl(&session)?);
}
(Some(_), Some(_)) => unreachable!("clap enforces conflicts_with"),
}
Ok(())
}
}

fn run_claude(input: String, project: Option<PathBuf>, output: Option<PathBuf>) -> Result<()> {
#[cfg(target_os = "emscripten")]
{
Expand Down
Loading