Skip to content

feat: add QwenPaw agent support - #694

Merged
wesm merged 3 commits into
kenn-io:mainfrom
gkld:feat/qwenpaw-agent
Jun 16, 2026
Merged

feat: add QwenPaw agent support#694
wesm merged 3 commits into
kenn-io:mainfrom
gkld:feat/qwenpaw-agent

Conversation

@gkld

@gkld gkld commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Adds the QwenPaw coding agent to the registry and sync pipeline.

QwenPaw stores daily conversation transcripts as
<workspace>/dialog/<YYYY-MM-DD>.jsonl under ~/.copaw/workspaces/.
Each runtime hosts multiple agent workspaces (default,
fund_manager, note_keeper, researcher, ...), and each workspace
logs one JSONL file per active day.

The parser handles Anthropic-style content blocks (text, thinking,
tool_use, tool_result) with one QwenPaw quirk: tool results live in
role: "system" messages rather than user messages. Those map to
RoleUser + IsSystem so they remain distinguishable from real user
turns without inflating UserMessageCount. Timestamps use the
"YYYY-MM-DD HH:MM:SS.fff" local-time format and are parsed via
time.ParseInLocation(..., time.Local), mirroring the Hermes parser.

Raw session IDs use the form <workspace>:<date>, yielding full IDs
like qwenpaw:default:2026-04-19. Discovery walks
<root>/<workspace>/dialog/*.jsonl; the watcher path classifier and
project extraction in internal/sync/engine.go were extended to match.

Scope / non-goals:

  • Only dialog/*.jsonl is parsed. The sessions/*.json agent-memory
    snapshots overlap with dialog content and are skipped for now.
  • inbox_traces/*.json (cron run traces) and inbox_events.json are
    notification-shaped, not conversations, and are out of scope.
  • Token usage aggregation from token_usage.json is not wired up; it
    is keyed by day/model rather than by session.

Reviewers should look at:

  • internal/parser/qwenpaw.go — discovery, source resolution, parse
    loop, role mapping, timestamp parsing.
  • internal/parser/qwenpaw_test.go — table-driven coverage of the
    happy path plus malformed lines, empty content, missing timestamps,
    multiple tool_use blocks, and the system-role tool_result mapping.
  • internal/sync/engine.go — four insertion points (watcher path
    classification, process dispatch switch, new processQwenPaw,
    project extraction case).
  • internal/parser/types.go — new AgentQwenPaw constant and
    registry entry (EnvVar: QWENPAW_DIR, DefaultDirs: [".copaw/workspaces"], IDPrefix: "qwenpaw:").
  • frontend/src/lib/utils/agents.ts — label/color entry (cyan,
    matching Qwen Code; can be re-tinted separately if a distinct
    visual identity is desired).

@roborev-ci

roborev-ci Bot commented Jun 15, 2026

Copy link
Copy Markdown

roborev: Combined Review (0d11c0f)

Summary verdict: one medium parser robustness issue should be fixed before merging.

Medium

  • Location: internal/parser/qwenpaw.go:122
  • Problem: The QwenPaw parser caps JSONL lines at 8 MiB and returns an error when a larger tool-result line is encountered, which can prevent syncing the whole QwenPaw session. Other JSONL parsers in this repo use the shared 64 MiB maxLineSize/lineReader path to tolerate large local agent outputs.
  • Fix: Use newLineReader(f, maxLineSize), or at least raise the scanner limit to the shared maximum and handle oversized lines consistently.

Panel: ci_default_security | Synthesis: codex, 8s | Members: codex_default (codex/default, done, 6m50s), codex_security (codex/security, done, 2m18s) | Total: 9m16s

@roborev-ci

roborev-ci Bot commented Jun 15, 2026

Copy link
Copy Markdown

roborev: Combined Review (3214b68)

Summary: Medium issues remain in the QwenPaw parser; no exploitable security issues were reported.

Medium

  • internal/parser/qwenpaw.go:315: FileInfo.Mtime is stored in seconds, while the sync engine and DB expect nanoseconds. After the first sync, unchanged QwenPaw files will not match shouldSkipByPath, causing repeated reparses/upserts and local-modified churn.

    • Fix: Use info.ModTime().UnixNano() and add coverage for the stored mtime unit.
  • internal/parser/qwenpaw.go:232: Tool results omit ContentLength, so paired tool calls persist result_content_length as zero. For default-blocked categories like Read, this loses the only retained result-size signal.

    • Fix: Populate ContentLength from block.Output using the existing tool-result length helper and assert it in the QwenPaw tool-result test.

Panel: ci_default_security | Synthesis: codex, 9s | Members: codex_default (codex/default, done, 8m56s), codex_security (codex/security, done, 2m35s) | Total: 11m40s

@roborev-ci

roborev-ci Bot commented Jun 15, 2026

Copy link
Copy Markdown

roborev: Combined Review (7b6c89a)

No Medium, High, or Critical findings were reported.

The security review found no issues. The remaining findings were Low severity and are omitted per the review-combination rules.


Panel: ci_default_security | Synthesis: codex, 5s | Members: codex_default (codex/default, done, 7m50s), codex_security (codex/security, done, 1m34s) | Total: 9m29s

@roborev-ci

roborev-ci Bot commented Jun 15, 2026

Copy link
Copy Markdown

roborev: Combined Review (6ea81db)

Medium confidence: one correctness issue should be fixed before merge; no security findings were reported.

Medium

  • internal/parser/qwenpaw.go:193 - QwenPaw session IDs omit the relative subdirectory, so sessions/foo.json and sessions/console/foo.json in the same workspace both become qwenpaw:<workspace>:foo. Because discovery supports both layouts, one session can overwrite the other during sync, and FindQwenPawSourceFile is ambiguous because it probes the root before console.

    Fix: Include the layout/subdirectory in the canonical raw ID for non-root sessions, or otherwise dedupe with an explicit precedence rule and make discovery/source lookup/classification use the same namespace.


Panel: ci_default_security | Synthesis: codex, 7s | Members: codex_default (codex/default, done, 4m42s), codex_security (codex/security, done, 2m54s) | Total: 7m43s

@mariusvniekerk

mariusvniekerk commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator

Can you include some santized sample qwenpaw files for this?

and ideally also a script that can be used to regenerate them using the cli of the agent?

Alternatively a stable link to where in the qwenpaw source code these sessions are serialized so that we have something to test against.

@roborev-ci

roborev-ci Bot commented Jun 16, 2026

Copy link
Copy Markdown

roborev: Combined Review (a6a593e)

Summary verdict: No Medium, High, or Critical findings to report.

The only findings reported were Low severity, so they are omitted per the review-combination rules.


Panel: ci_default_security | Synthesis: codex, 4s | Members: codex_default (codex/default, done, 5m55s), codex_security (codex/security, done, 2m53s) | Total: 8m52s

@roborev-ci

roborev-ci Bot commented Jun 16, 2026

Copy link
Copy Markdown

roborev: Combined Review (228eca3)

High-risk compile blockers remain in the QwenPaw integration; no medium-or-higher security findings were reported.

High

  • internal/parser/types.go:472: The QwenPaw registry entry is not closed before the gptme fields begin, leaving duplicate keyed fields in one AgentDef literal. This is a Go compile error and prevents the registry from representing both agents correctly.

    • Fix: Add a separate }, { boundary after FindSourceFunc: FindQwenPawSourceFile, so QwenPaw and gptme are distinct registry entries.
  • internal/sync/engine.go:6194: The QwenPaw SyncSingleSessionContext case contains an unresolved merge with Kimi project-resolution code. It declares rel/ok without using them, references parts outside its scope, and can fall into the Kimi default, so this block cannot compile and sync-single project resolution is broken.

    • Fix: Separate the Kimi and QwenPaw cases cleanly; derive QwenPaw project from the first path segment relative to the configured QwenPaw root, and keep Kimi’s existing decode/default logic in the Kimi case.

Panel: ci_default_security | Synthesis: codex, 11s | Members: codex_default (codex/default, done, 4m0s), codex_security (codex/security, done, 3m5s) | Total: 7m16s

@roborev-ci

roborev-ci Bot commented Jun 16, 2026

Copy link
Copy Markdown

roborev: Combined Review (f727026)

Medium-risk issue found; no Critical or High findings.

Medium

  • internal/parser/qwenpaw.go:161 - IsValidQwenPawIDPart accepts . and .., and FindQwenPawSourceFile passes those parts directly into filepath.Join, allowing crafted raw IDs to resolve outside the configured QwenPaw root or outside sessions/.
    • Fix: Reject . and .. for every ID part, or clean the candidate path and verify it remains under the expected QwenPaw sessions directory before returning it.

Panel: ci_default_security | Synthesis: codex, 7s | Members: codex_default (codex/default, done, 6m21s), codex_security (codex/security, done, 2m57s) | Total: 9m25s

@roborev-ci

roborev-ci Bot commented Jun 16, 2026

Copy link
Copy Markdown

roborev: Combined Review (a5690e1)

Medium finding blocks a fully clean review; no High or Critical issues were reported.

Medium

  • Location: internal/parser/qwenpaw.go:271
  • Problem: ParseQwenPawSession extracts fields with gjson.GetBytes before validating the file as JSON. A truncated or malformed document could still be partially accepted if agent.memory.content is discoverable before the syntax error, storing incomplete or stale session data while treating the source as successfully parsed.
  • Fix: Validate with gjson.ValidBytes(raw) before reading fields and return an invalid JSON error; update malformed/non-message tests so successful cases use valid JSON.

Panel: ci_default_security | Synthesis: codex, 7s | Members: codex_default (codex/default, done, 4m35s), codex_security (codex/security, done, 4m10s) | Total: 8m52s

@gkld

gkld commented Jun 16, 2026

Copy link
Copy Markdown
Contributor Author

Can you include some santized sample qwenpaw files for this?

and ideally also a script that can be used to regenerate them using the cli of the agent?

Alternatively a stable link to where in the qwenpaw source code these sessions are serialized so that we have something to test against.

QwenPaw:https://github.com/agentscope-ai/QwenPaw
commit:68e023d

@mjacobs

mjacobs commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Took a review pass on this (file-based agent, so the heavy DB-backed sync hazards mostly don't apply). The roborev bot already drove out the serious mechanical defects commit-by-commit — the compile-breaking registry/merge conflict, path traversal, mtime-in-seconds, read_file ContentLength, and the root-vs-console ID collision are all fixed in the current head. Below are the items a generic pass wouldn't catch. None are hard blockers, but #1 is worth resolving (or consciously waiving) before merge.

What's good

Clean separation of discovery / find-source / parse. The subdir-in-ID fix is correct and well-tested; traversal hardening (isUnderQwenPawRoot + IsValidQwenPawIDPart) is thorough; mtime is nanoseconds with a regression test; read_file already lands in NormalizeToolCategory/formatToolUse as Read. The testdata fixtures cover the genuinely awkward cases (channel-scoped @/dot filenames, the console subdir, empty content). Test coverage is strong.

1. Message writes use the append-only path, but QwenPaw rewrites the whole file (the one to resolve)

processQwenPaw doesn't set forceReplace, and AgentQwenPaw isn't in the replaceMessages gate (internal/sync/engine.go:5050). So on every re-sync it takes the append-only writeMessages path, which only inserts ordinals greater than the max already stored and never updates or deletes existing rows.

That path is correct only for strictly append-only sources with stable ordinals (Claude/Codex JSONL). QwenPaw isn't that shape:

  • QwenPaw's _atomic_write_json rewrites the entire session JSON on each save (temp file + os.replace).
  • agent.memory.content is an agent-memory snapshot, and the parser assigns Ordinal by position in that array on each parse.

If that array is ever compacted, summarized, reordered, or has its latest message finalized in place — all common in agent-memory frameworks, and plausible since every save rewrites the whole file — ordinals shift and the append path silently keeps stale/misaligned rows. This is the hazard the engine comment at engine.go:5019 calls out ("compact-boundary messages can shift the ordinal stream — demand a full rewrite rather than the append-only writeMessages path"), and it's why every other whole-file-rewrite source (OpenCode, Antigravity, AntigravityCLI) is in the replaceMessages gate.

Suggestion: default processQwenPaw to forceReplace: true (cheap — the file is rewritten wholesale anyway), unless you can confirm from QwenPaw's memory module that content is strictly append-only with stable ordinals across a session's life.

2. PR description is stale / inverted

The description still describes the original implementation: sessions stored as dialog/<YYYY-MM-DD>.jsonl, discovery walking dialog/*.jsonl, and — in Scope/non-goals — "Only dialog/*.jsonl is parsed. The sessions/*.json agent-memory snapshots … are skipped." The code does the opposite: it parses sessions/*.json and skips dialog/*.jsonl.

More than a doc nit: the pivot from dialog/ (append-only daily logs) to sessions/ (memory snapshot) is the substantive design decision, and it's the same question as #1 — the append log is the more complete record if memory ever compacts. A sentence on why sessions/ was chosen over the append-only dialog/ logs would help reviewers.

3. gen.py overclaims fidelity

The README and gen.py docstring say the script "drives QwenPaw's own session serializer so the fixtures stay byte-for-byte consistent with what a live runtime writes." It doesn't — it builds the dicts by hand and writes them with json.dumps(..., indent=2) + "\n"; the QwenPaw module is only imported to sanity-check the source path. The real serializer emits compact JSON (json.dumps(..., ensure_ascii=False), no indent), so the fixtures aren't byte-identical to real output. The stable source link in the thread already satisfies the original ask, so the cleanest fix is to soften the claim (e.g. "hand-authored to match the documented shape"), or actually round-trip messages through save_session_state to get the true byte shape.

Minor / nits

  • roborev's last open item (gjson.ValidBytes(raw) before reading fields, commit a5690e1): TestParseQwenPawSession_MalformedJsonReturnsError currently passes for the wrong reason — {not valid json errors via "content missing," not invalid-JSON detection. A one-line ValidBytes guard would make the test assert what it claims.
  • No token usage is extracted (documented non-goal). Worth confirming QwenPaw's Anthropic-style assistant blocks don't carry a per-message usage field that's being dropped — if they do, capturing it is cheap; otherwise cost analytics show zero for all QwenPaw sessions.
  • Add an AgentByPrefix routing test for qwenpaw: vs qwen:. They're disjoint today ("qwenpaw:" doesn't HasPrefix "qwen:"), but that's exactly the near-collision a test should lock in.
  • Registry-completeness: the frontend agents.test.ts was updated, but I don't see qwenpaw added to the Go allTypes list (internal/parser/types_test.go). Adding the entry is free.
  • Serve path with @/. IDs: no Pattern validation on the {id} route param and DB lookup is exact-match, so channel-scoped IDs like qwenpaw:default:user@example.com_… serve fine (Kimi/OpenClaw already use colon-bearing IDs). Low risk; a quick frontend URL spot-check wouldn't hurt.

@roborev-ci

roborev-ci Bot commented Jun 16, 2026

Copy link
Copy Markdown

roborev: Combined Review (e70856d)

Medium findings:

  • internal/sync/engine.go:6218 - Single-session resync derives the QwenPaw workspace only from currently configured QWENPAW_DIR roots. If FindSourceFile returns a stored DB file_path from a previously configured/custom root, no root matches, file.Project stays empty, and ParseQwenPawSession writes a new qwenpaw::... session instead of refreshing the requested one.
    Fix: fall back to the existing DB session project or parse the workspace from the requested sessionID when the path is not under a configured QwenPaw root.

  • scripts/qwenpaw-fixtures/gen.py:67 - _import_serializer prepends the user-supplied --qwenpaw-src/src directory to sys.path and imports qwenpaw.app.runner.session. With a malicious or compromised QwenPaw checkout, Python executes arbitrary top-level code from that tree during fixture regeneration.
    Fix: avoid importing from --qwenpaw-src; check expected files/symbols via path/text/AST inspection, or explicitly document and isolate execution if importing QwenPaw code is required.


Panel: ci_default_security | Synthesis: codex, 15s | Members: codex_default (codex/default, done, 12m11s), codex_security (codex/security, done, 5m25s) | Total: 17m51s

@wesm

wesm commented Jun 16, 2026

Copy link
Copy Markdown
Member

I can take it from here if that's ok

@roborev-ci

roborev-ci Bot commented Jun 16, 2026

Copy link
Copy Markdown

roborev: Combined Review (619c760)

Review verdict: changes need attention for two medium-severity issues before merge.

Medium

  • internal/parser/qwenpaw.go:221
    QwenPaw ID components are concatenated with : without escaping or rejecting : in workspace/subdir/stem names. Distinct valid paths like sessions/foo:bar.json and sessions/foo/bar.json can collapse to the same session ID, and source lookup will interpret the root file as the subdir layout.
    Fix: Reject : in every path-derived ID component during discovery/classification, or encode components before building IDs and decoding them in FindQwenPawSourceFile.

  • scripts/qwenpaw-fixtures/gen.py:267
    The fixture generator resolves the user-controlled output path before deleting it. If <out>/qwenpaw is a symlink, .resolve() follows it and shutil.rmtree(out_root) deletes the symlink target at line 269. An attacker who can pre-create that symlink in an output parent the developer uses, such as a shared temp directory, or land it in a malicious checkout, can cause arbitrary developer-writable directory deletion when the script is run.
    Fix: Keep the deletion path unresolved, reject symlinks with lstat / Path.is_symlink() before rmtree, and optionally require the resolved destination to remain under the intended fixture parent before rebuilding it.


Panel: ci_default_security | Synthesis: codex, 14s | Members: codex_default (codex/default, done, 17m38s), codex_security (codex/security, done, 6m58s) | Total: 24m50s

@wesm
wesm force-pushed the feat/qwenpaw-agent branch from 619c760 to b4c0e19 Compare June 16, 2026 13:06
@roborev-ci

roborev-ci Bot commented Jun 16, 2026

Copy link
Copy Markdown

roborev: Combined Review (b4c0e19)

Review verdict: changes need attention before merge due to two Medium issues.

Medium

  • internal/parser/qwenpaw.go:210
    QwenPaw ID parts are joined directly into session IDs, but validation still allows ~ and URL delimiters like ?, #, and %. ~ is treated by the backend as the remote-host separator, and the frontend/generated path client may not safely escape all URL delimiters, so imported files with those characters can produce session IDs that are misclassified or cannot be opened reliably.
    Suggested fix: Encode QwenPaw ID components and decode them in source lookup, or restrict IsValidQwenPawIDPart to a conservative backend- and URL-safe alphabet such as alphanumerics plus ._@-. Add tests for ~, ?, #, and %.

  • scripts/qwenpaw-fixtures/gen.py:267
    out_root = (args.out / "qwenpaw").resolve() follows symlinks before shutil.rmtree(out_root) deletes the tree. If a contributor-controlled checkout or local attacker can make internal/parser/testdata/qwenpaw a symlink, the documented regeneration command resolves that symlink and recursively deletes the target under the developer or CI user’s permissions.
    Suggested fix: Do not resolve the deletion target before removing it. Use lstat/Path.is_symlink() to reject symlinks for args.out, args.out/qwenpaw, and relevant parents, and constrain deletion to the intended fixture directory before calling rmtree.


Panel: ci_default_security | Synthesis: codex, 16s | Members: codex_default (codex/default, done, 11m27s), codex_security (codex/security, done, 2m59s) | Total: 14m42s

@roborev-ci

roborev-ci Bot commented Jun 16, 2026

Copy link
Copy Markdown

roborev: Combined Review (e2527a0)

No Medium, High, or Critical findings were reported.

The only reported findings were Low severity, so they are omitted per the requested threshold.


Panel: ci_default_security | Synthesis: codex, 4s | Members: codex_default (codex/default, done, 5m31s), codex_security (codex/security, done, 3m29s) | Total: 9m4s

gkld and others added 2 commits June 16, 2026 10:12
- Use shared lineReader for QwenPaw parser
- Store QwenPaw mtime in nanoseconds and populate tool-result length
- Read QwenPaw sessions from sessions/, not dialog/
- Drop redundant compactJSON to satisfy modernize linter
- Encode QwenPaw sessions/ subdir in session ID to prevent collisions
- Add sanitized QwenPaw fixtures and regeneration script
- Reject path traversal in QwenPaw source resolution
- Route QwenPaw sessions through full-replace write path
- Preserve QwenPaw workspace on resync and harden fixture generator
- Reject ":", "~", and URL delimiters in ID parts across discovery,
  parsing, and source lookup to prevent ambiguous/unopenable IDs
- Harden the fixture generator against symlinked deletion targets

Co-Authored-By: Wes McKinney <wesmckinn+git@gmail.com>
Keep the traversal test's escape file inside the t.TempDir() tree
instead of writing above root. Add TestQwenPawFixtures so the
checked-in testdata/qwenpaw fixtures are discovered, parsed, and
asserted in CI, catching malformed or drifting fixtures.
@wesm
wesm force-pushed the feat/qwenpaw-agent branch from e2527a0 to ddc37f3 Compare June 16, 2026 17:22
@roborev-ci

roborev-ci Bot commented Jun 16, 2026

Copy link
Copy Markdown

roborev: Combined Review (ddc37f3)

No issues found.


Panel: ci_default_security | Synthesis: codex | Members: codex_default (codex/default, done, 7m4s), codex_security (codex/security, done, 3m44s) | Total: 10m48s

':' is illegal in Windows filenames, so tests that created colon-named
files or directories failed on windows-latest CI. Make the workspace
parse-validation case cross-platform by carrying ':' in the project
argument instead of a directory name, and skip the cases that require a
colon on disk (subdir derivation, collision discovery, classify scan)
on Windows. TestIsValidQwenPawIDPart still covers the rejection logic
on every platform.
@roborev-ci

roborev-ci Bot commented Jun 16, 2026

Copy link
Copy Markdown

roborev: Combined Review (0defba7)

Summary verdict: No Medium, High, or Critical findings to report.

The only reported issue was Low severity, so it is omitted under the requested threshold. The security review found no exploitable security issues.


Panel: ci_default_security | Synthesis: codex, 13s | Members: codex_default (codex/default, done, 6m19s), codex_security (codex/security, done, 2m12s) | Total: 8m44s

@wesm
wesm merged commit ca76233 into kenn-io:main Jun 16, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

4 participants