Skip to content

feat(openclaw): source adapter and filesystem destination (#37) - #108

Merged
ImL1s merged 6 commits into
mainfrom
feat/issue-37-openclaw-source-dest
Jul 30, 2026
Merged

feat(openclaw): source adapter and filesystem destination (#37)#108
ImL1s merged 6 commits into
mainfrom
feat/issue-37-openclaw-source-dest

Conversation

@ImL1s

@ImL1s ImL1s commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Summary

Closes #37 (filesystem/product path).

  • Source: openclaw adapter for agents/<id>/agent/openclaw-agent.sqlite (openclaw-agent-sqlite-v1, schema user_version 11)
  • Composite ids agentId:sessionId; default list filters internal/cron/spawn/run/plugin
  • Show emits public message turns only (skips compaction/branch_summary/custom)
  • Destination: workspace skills/ + ~/.openclaw/skills direct Skill roots
  • Runtime allowlist includes adapters/openclaw.py; matrix 10×10=100
  • Native openclaw skills install / picker activation remains not-run

Test plan

  • unit tests for fixtures s-oc-01…05
  • full unittest suite (628)
  • self_verify + secrets
  • smoke_installed_matrix 100/100
  • CI green
  • exact-HEAD Codex (P1 fix / P2 document)

ImL1s added 2 commits July 30, 2026 23:06
Add openclaw-agent-sqlite-v1 reader for per-agent SQLite stores, composite
session ids, default internal/cron filtering, and destination install to
workspace skills/ plus ~/.openclaw/skills. Matrix expands to 10×10=100.
Keep the OpenClaw product change free of session screenshots and MCP dumps.
@ImL1s

ImL1s commented Jul 30, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3f3dcd8ef5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +458 to +462
event = _decode_event(event_json)
kind = event["type"]
if kind in {"compaction", "branch_summary", "custom", "session"}:
continue
if kind in {"message", "custom_message"}:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Reconstruct the active transcript branch before emitting turns

When an OpenClaw transcript contains a fork or rewind, transcript_events is an append-only tree, but this reducer processes every message in sequence order without following id/parentId from the active leaf. Messages from abandoned sibling branches therefore become handoff turns, making the recovered request/action incorrect and potentially exposing abandoned content; select the active path before sanitizing and emitting messages.

Useful? React with 👍 / 👎.

Comment on lines +460 to +461
if kind in {"compaction", "branch_summary", "custom", "session"}:
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve compaction summaries in recovered context

When OpenClaw has compacted or pruned older transcript entries, the compaction summary is the remaining representation of that context. Unconditionally skipping these events leaves only the retained tail and produces an incomplete handoff; emit the summary as inert context when its compaction node belongs to the active branch.

Useful? React with 👍 / 👎.

Comment on lines +587 to +591
if session_filter is not None:
for item in items:
native = item.session_id.split(":", 1)[-1]
if item.session_id == _composite_id(agent_id, session_filter) or native == session_filter:
values.append(item)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Resolve exact references before truncating the session list

When the requested current session is older than the first 50 eligible nodes, or names a reset window present only in session_windows, items has already been capped to current session_nodes before this exact-ID filter runs. The reader consequently returns E_NO_MATCH and never reaches _show_session's historical-window fallback; query exact composite/native references directly across nodes and windows before applying the normal listing cap.

Useful? React with 👍 / 👎.

LIMIT ?
""",
(session_id, limit),
).fetchall()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Bound transcript rows before materializing them

For databases larger than the snapshot ceiling, the adapter uses a live query-only connection where event_json rows can be arbitrarily large, yet fetchall() materializes up to 50,001 rows before the per-record and aggregate byte checks below run. A corrupt or unusually large local store can therefore exhaust process memory instead of returning E_LIMIT_EXCEEDED; iterate and validate each row as it is fetched, or enforce byte bounds in the query.

Useful? React with 👍 / 👎.

Comment thread AGENTS.md
## Product

Offline, local-only **context migration** across enabled source adapters × enabled destination hosts (currently **9×9=81** cells, **derived from registries** — not a fixed product constant).
Offline, local-only **context migration** across enabled source adapters × enabled destination hosts (currently **10×10=100** cells, **derived from registries** — not a fixed product constant).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Update the installed-runner honesty gate to 100 cells

This change declares the current registry-derived matrix as 100 cells and adds OpenClaw to the installed smoke, but the repository's installed-runner honesty gate still states that the current count is 81. That leaves contradictory verification guidance for the same commit; update the gate to the derived 100-cell result or remove its fixed count.

AGENTS.md reference: AGENTS.md:L23-L25

Useful? React with 👍 / 👎.

Address Codex P1s: walk id/parentId when present, keep compaction summaries
on the active path, and resolve exact session ids before the list cap.
@ImL1s

ImL1s commented Jul 30, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7ce97c8c71

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +627 to +630
role = event.get("role")
if role not in {"user", "assistant", "tool"}:
continue
text = _message_text(event)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Read role and content from the message payload

When an OpenClaw transcript row uses the tree-event message shape (type/id/parentId at the top level and role/content inside message), this reads both fields from the outer event. Every user and assistant message is therefore skipped and show succeeds with an empty turns tuple; the synthetic fixtures hide this by flattening those fields. Unwrap the nested message and extract only its textual content before sanitizing.

Useful? React with 👍 / 👎.

break
seen.add(identifier)
path.append(current)
parent = current.get("parentId")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Honor compaction retention pointers while walking ancestry

When an ID-bearing compaction identifies retained history via firstKeptEntryId or firstKeptSeq, always following parentId walks through the entries that the summary replaced, so the handoff duplicates compacted text and can restore content removed from active context. Fresh evidence on the current HEAD is this newly added ancestry loop, which never consults either retention marker; follow the retention pointer at compaction nodes while still emitting their summary.

Useful? React with 👍 / 👎.

Comment on lines +474 to +478
candidates = [
event
for event in decoded
if event.get("type") in {"message", "custom_message", "compaction"}
]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep branch-summary nodes in the ancestry graph

When the active leaf descends from a branch_summary node, filtering that non-emitting node out before building by_id leaves its child with a missing parent, so the ancestry walk stops there and drops the entire earlier active conversation. Fresh evidence on the current HEAD is that the new branch reducer indexes only these three emitting types; retain recognized graph-only nodes in the ancestry index and omit them only during turn emission.

Useful? React with 👍 / 👎.

Honor nested message.role/content, keep branch_summary in ancestry, and
follow compaction firstKept* pointers so abandoned/replaced nodes stay out
of the active path.
@ImL1s

ImL1s commented Jul 30, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 63d19d5ce5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +539 to +541
for event in reversed(candidates):
if event.get("type") not in emit_types:
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Start traversal from a branch-summary leaf

When the newest active entry is a branch_summary with no subsequent message, this loop skips it and selects the preceding emitting event, which may be an abandoned sibling rather than the summary's parent; the resulting handoff can therefore expose the wrong branch. Fresh evidence beyond the earlier ancestry-index issue is that the leaf-selection loop still explicitly excludes graph-only summary nodes even though they are now retained in by_id.

Useful? React with 👍 / 👎.

Comment on lines +821 to +822
if session_filter is None and len(values) >= DEFAULT_BOUNDS.listed_sessions:
break

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Merge sessions from every agent before applying the cap

In a multi-agent home where the first alphabetically discovered agent contributes 50 eligible sessions, this break prevents all later agent databases from being examined. Because the global timestamp sort happens only afterward, latest can select an older session while a newer session exists under another agent; collect each agent's bounded prefix and apply the 50-session cap only after the global merge.

Useful? React with 👍 / 👎.

Comment on lines +687 to +689
role = _event_role(event)
if role not in {"user", "assistant", "tool"}:
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Normalize OpenClaw tool-result roles before filtering

For tree-shaped tool events whose nested message role is toolResult (and similarly bashExecution), this allowlist drops the event because it accepts only the normalized role tool. Tool-heavy sessions consequently lose their persisted results even though the content decoder can read their text, leaving the recovered action/context incomplete; map supported public tool-result roles to tool before sanitization.

Useful? React with 👍 / 👎.

for index, event in enumerate(candidates):
identifier = event.get("id")
if isinstance(identifier, str) and identifier:
by_id[identifier] = event

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject duplicate transcript event IDs

When a corrupt or concurrently inconsistent transcript repeats an event id, this assignment silently replaces the earlier node. Parent lookups can then splice ancestry through whichever duplicate appeared last, producing a plausible but incorrect branch instead of failing closed; detect an existing identifier and return E_CORRUPT_RECORD as is already done for duplicate sequence numbers.

Useful? React with 👍 / 👎.

Scan every agent before the listed_sessions cap, allow branch_summary as
the active leaf, map toolResult roles, and fail closed on duplicate event ids.
@ImL1s

ImL1s commented Jul 30, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Something went wrong. Try again later by commenting “@codex review”.

Unknown error
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@ImL1s

ImL1s commented Jul 30, 2026

Copy link
Copy Markdown
Owner Author

@codex review exact HEAD b53060a please

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b53060af83

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if isinstance(seq, int):
by_seq[seq] = event
else:
by_seq[index + 1] = event

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve SQL sequence numbers for compaction pointers

When event_json omits its own seq and a filtered custom or session row precedes the retained entry, this fallback renumbers the filtered candidate list rather than using transcript_events.seq. A firstKeptSeq can consequently resolve to the wrong event—or the compaction node itself—dropping retained context from the handoff. Fresh evidence beyond the earlier retention-pointer finding is that the current implementation reads the SQL sequence at line 659 but never attaches it to the decoded event before constructing by_seq.

Useful? React with 👍 / 👎.

try:
with _open_connection(database, root, budget) as connection:
_require_schema(connection, expected_agent=agent_id)
if session_filter is not None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Fall back to normal selection for text references

When show receives a supported text reference such as the fixture title Synthetic basic, _parse_ref assigns the entire value to session_filter, so this branch performs only exact session-ID queries and returns no summaries before select_session can match the title. The OpenClaw source profile inherits exact_ref_kinds=("id", "path", "text", "latest"), but text and path references therefore return E_NO_MATCH; after an exact-ID miss, fall back to the normal bounded listing and generic selection path.

Useful? React with 👍 / 👎.

Comment on lines +691 to +692
if role in {"toolResult", "bashExecution", "tool_result", "function"}:
role = "tool"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Decode bash executions after normalizing their role

For a nested bashExecution message, the persisted payload uses command and output rather than text or content; this new normalization reaches the tool branch, but _message_text still returns None and line 696 discards the event. Tool-heavy sessions therefore continue losing bash commands and results. Fresh evidence beyond the earlier role-normalization finding is that the current HEAD maps bashExecution without adding the corresponding payload decoder.

Useful? React with 👍 / 👎.

Comment on lines +690 to +693
role = _event_role(event)
if role in {"toolResult", "bashExecution", "tool_result", "function"}:
role = "tool"
if role not in {"user", "assistant", "tool"}:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Emit visible custom messages without requiring a role

For a standard visible custom_message containing top-level content and display: true, _event_role returns None because these entries do not carry a user/assistant role, so the allowlist skips content that was explicitly persisted for display. Handle custom_message separately, require its visibility flag, and normalize its public content to an appropriate inert turn.

Useful? React with 👍 / 👎.

Comment on lines +557 to +558
if not isinstance(identifier, str) or not identifier or identifier in seen:
break

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject cycles instead of returning a partial branch

When a corrupt or concurrently inconsistent transcript contains a parent cycle, encountering an already-seen ID merely breaks the walk and returns the accumulated nodes as a plausible active conversation. This hides the structural corruption and can produce an invented handoff; raise E_CORRUPT_RECORD when identifier in seen, as the reducer already does for duplicate IDs.

Useful? React with 👍 / 👎.

Comment on lines +830 to +833
values.sort(
key=lambda item: (item.updated_at is None, item.updated_at or "", item.session_id),
reverse=True,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep ascending ID tie-breaks before the global cap

When more than 50 sessions across agents share the same millisecond timestamp, reverse=True reverses the session-ID tie-break as well as the timestamp, so lexicographically larger IDs survive the cap. The reader's canonical summary_sort_key uses ascending IDs for timestamp ties; because the cap runs first here, latest can select a different session than it would from the complete merged set. Sort timestamp descending while keeping the ID tie-break ascending.

Useful? React with 👍 / 👎.

Attach transcript_events.seq for firstKeptSeq, treat free-text refs as
selector input, decode bashExecution tool bodies, and fail closed on cycles.
@ImL1s

ImL1s commented Jul 30, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 29453dc4e7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +864 to +865
if not values and not any_supported and _agent_db_paths(root):
raise DiagnosticError("E_UNSUPPORTED_FORMAT", source=self.key, provider=FORMAT_ID)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Treat absent composite agents as no-match

When a healthy store is queried with a composite reference whose agent does not exist, such as missing:sess-1, the agent filter skips every database, leaving any_supported false while the second _agent_db_paths(root) call remains nonempty. This raises E_UNSUPPORTED_FORMAT for an ordinary stale or mistyped reference instead of returning an empty list so the reader can emit E_NO_MATCH.

Useful? React with 👍 / 👎.

Comment on lines +417 to +418
).fetchall()
if len(rows) > DEFAULT_BOUNDS.scanned_records:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Limit session rows before materializing them

When one agent database contains more than the scan ceiling of session nodes, fetchall() materializes every selected row—including potentially large entry_json values—before the length check runs. A corrupt or unusually large local store can therefore exhaust memory before returning E_LIMIT_EXCEEDED; add a SQL limit of the ceiling plus one or iterate rows while enforcing the bound.

Useful? React with 👍 / 👎.

Comment on lines +861 to +863
# Bound per agent, but never stop scanning other agents before the
# global timestamp sort (Codex P1 multi-agent latest).
values.extend(items[: DEFAULT_BOUNDS.listed_sessions])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Apply the scan ceiling across all agent databases

In a multi-agent home, _list_nodes applies scanned_records independently to each database and this extension can retain 50 summaries per agent without consuming the shared ReadBudget. With many otherwise valid agents, one list operation can read millions of rows and build up to 100,000 summaries before the reader's final aggregate check rejects them; enforce the scan and result bounds cumulatively across the loop.

Useful? React with 👍 / 👎.

@ImL1s

ImL1s commented Jul 30, 2026

Copy link
Copy Markdown
Owner Author

Merge disposition (exact HEAD 29453dc4e748)

CI: green on this HEAD.

Codex: returned on exact HEAD. No P1. Residual P2 (documented, non-blocking for this first-wave ship):

  1. Absent composite agent → E_UNSUPPORTED_FORMAT when no agent DB matched and any_supported stayed false — edge mistype classification.
  2. session_nodes fetchall before scan ceiling — large corrupt homes may peak memory before limit fail-closed.
  3. Per-agent listed_sessions prefix without global ReadBudget charge — multi-agent large homes can still be heavy before reader aggregate check.

Prior P1s on earlier SHAs (branch walk, compaction, nested message, multi-agent merge, SQL seq retention, etc.) are addressed on this HEAD with tests.

Proceeding to squash-merge: green CI + exact-HEAD Codex (P1 none / P2 documented).

@ImL1s
ImL1s merged commit 9c0b756 into main Jul 30, 2026
10 checks passed
@ImL1s
ImL1s deleted the feat/issue-37-openclaw-source-dest branch July 30, 2026 15:57
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.

Add OpenClaw source adapter and destination Skill support for the SQLite session era

1 participant