Skip to content

feat(session): low-yield repeated probing detection#264

Merged
Astro-Han merged 14 commits into
devfrom
claude/issue-229-loop-detection-v1
Apr 27, 2026
Merged

feat(session): low-yield repeated probing detection#264
Astro-Han merged 14 commits into
devfrom
claude/issue-229-loop-detection-v1

Conversation

@Astro-Han

@Astro-Han Astro-Han commented Apr 27, 2026

Copy link
Copy Markdown
Owner

Summary

Add a deterministic pre-execution gate that stops repeated tool failures within one user turn. After 3 same-input or same-target failures we inject a recovery reminder; if the model keeps probing, the gate writes a synthetic block tool part on attempt 6 (autoResume budget unspent), and on attempt 7 (budget spent) writes a synthetic stop tool part plus an assistant text part rendered in Chinese, ending the turn.

Both block and stop are propagated symmetrically via Effect.fail (not raw throw), so the existing ctx.blocked break path terminates the prompt loop cleanly. A re-entry guard prevents the stop synthesis from firing twice if ai-sdk auto-resumes past the stop tool-error.

Why

Closes #229. Models like Kimi K2.6 sometimes fall into a low-yield probing loop (e.g., repeatedly fetching a 404 URL with minor variations) that wastes tokens and time without making progress. PR #204 added per-tool reminders but only injected at message scope; the model could ignore them. v1 escalates: reminder → block (auto-resume once) → stop, with a quiet user-facing summary so the user understands why the turn ended.

Related Issue

Refs #229. Spec is locked in comment 4322354892.

How To Verify

bun --cwd packages/opencode test test/session/
bun --cwd packages/opencode tsc --noEmit

Session test suite: 368 pass / 0 fail / 4497 expect calls. New tests:

  • test/session/loop-gate.test.ts (10 cases) covers deriveParentLoopState + queryGateAction for every gate-decision branch.
  • test/session/loop-renderer.test.ts (8 cases) covers webfetch input/target, generic non-webfetch, degraded templates (no <unknown> placeholder), bare-string URLs, and 1024-byte truncation.
  • test/session/diagnostics.test.ts extended with truncateForRenderer, targetHashIsFallback, signature-keyed firing, and lastInput/lastError persistence.
  • test/session/export.test.ts adds Export.deriveSnapshotDiagnostics cases for block snapshot + empty fallback.
  • test/session/prompt-effect.test.ts adds an end-to-end scenario driving 7 same-signature read calls through the loop and asserts: 0 completed tool parts, exactly 1 block tool part with loopCompletedFailures: 5, exactly 1 stop tool part with halted by PawWork error, an immediately-following synthetic text part containing the rendered Chinese summary, no trailing tool parts, recover reminder firing visible in the LLM input on the 4th attempt, and the system-reminder text injected verbatim.

The deleted loop injects diagnostics reminder after repeated tool input test in prompt-effect.test.ts asserted PR #204 behavior (reminder fires after 3 successful repeats); v1 explicitly removes firing on success, so the test is obsolete.

Screenshots or Recordings

Not applicable. Stop renderer output is plain assistant text with no PawWork-specific styling; it renders through the existing message-bubble path (zh fallback chain in packages/app/src/styles).

Checklist

  • I linked the related issue, or stated why there is no issue
  • This PR has type, scope, and priority labels, or I requested maintainer labeling
  • I listed the relevant verification steps, including tests when behavior changed
  • I manually checked visible UI or copy changes when needed, with screenshots or recordings
  • I considered macOS and Windows impact for desktop, packaging, updater, signing, paths, shell, or permissions changes
  • I called out docs, release notes, dependencies, permissions, credentials, deletion behavior, or generated/local file changes when relevant
  • I am targeting `dev`, and my PR title and commit messages use Conventional Commits in English

Summary by CodeRabbit

  • New Features

    • Target-aware loop gating that can block or stop repeated failing tool calls, halts resumed structured outputs, and records repeat-type ("input" vs "target") for diagnostics.
    • Improved stop/block summaries with redaction, truncation, and Chinese/English localization.
    • Snapshot exports now include the latest loop diagnostic for post-run inspection.
  • Bug Fixes

    • More accurate persistence of last-input/last-error, one-time recovery reminders per signature, and fewer spurious reminders for repeated successful calls.
  • Tests

    • Expanded coverage for gating, rendering, snapshot diagnostics, reminders, and recovery.

@Astro-Han Astro-Han added enhancement New feature or request P1 High priority harness Model harness, prompts, tool descriptions, and session mechanics labels Apr 27, 2026
@coderabbitai

coderabbitai Bot commented Apr 27, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@Astro-Han has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 11 minutes and 13 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 2f47c10d-418f-466f-b4eb-cc0b8e5dceb1

📥 Commits

Reviewing files that changed from the base of the PR and between 345aa8d and a3a84f4.

📒 Files selected for processing (11)
  • packages/opencode/src/session/diagnostics.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/src/session/processor.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/test/session/loop-gate.test.ts
  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts
📝 Walkthrough

Walkthrough

Adds target-aware, signature-scoped loop detection and gating: per-signature input/target hashing, richer loop metadata and recovery bookkeeping, gate decision helpers, pre-execution synthetic block/stop recording, localized stop rendering, and snapshot export of the most-recent loop event.

Changes

Cohort / File(s) Summary
Core Loop Diagnostics
packages/opencode/src/session/diagnostics.ts
Reworks diagnostics to track per-signature input/target hashes and last payloads; adds target_repeat, SignatureKind, LoopAction, SignatureState, ParentLoopState, GateDecision; extends LoopMetadata/ToolErrorRecord with gate fields and snapshots; changes targetSummary to return {summary,isFallback}; adds helpers truncateForRenderer, firstLine, deriveParentLoopState, queryGateAction; changes reminder handling and recovery firing tracking.
Loop Renderer
packages/opencode/src/session/loop-renderer.ts
New LoopRenderer.render producing localized (zh/en) “stopped after repeated failures” summaries; scrubs URLs/credentials/quoted strings/file paths, strips query/fragments for webfetch, truncates outputs.
Processor APIs & Synthetic Events
packages/opencode/src/session/processor.ts
Extends Handle with loop inspection and synthetic writers (errorRecords, syntheticBlockSigKeys, hasStopped, buildLoopContext, recordSyntheticBlock, recordSyntheticStop); enriches tool error records with input/target hashes and last snapshots; settles non-running tool calls, idempotent synthetic persistence, and stops LLM streaming when blocked.
Prompt / Tool Gate
packages/opencode/src/session/prompt.ts
Adds applyLoopGate run before tool execution to derive parent state, compute hashes, call queryGateAction, and persist synthetic block/stop (throws BlockedLoopError/LoopStopError to short-circuit). Threads shouldHalt into createStructuredOutputTool to prevent auto-resume output after synthetic stop.
Snapshot Export
packages/opencode/src/session/export.ts
Adds deriveSnapshotDiagnostics(node) and updates Export.Snapshot.diagnostics to optionally include loop.last with `{ parentID, type: "same_input"
Tests — Diagnostics, Gate & Renderer
packages/opencode/test/session/diagnostics.test.ts, packages/opencode/test/session/loop-gate.test.ts, packages/opencode/test/session/loop-renderer.test.ts
Updates and adds tests for per-signature recovery (loopRecoverFiredFor), persisted lastInput/lastError, targetHashIsFallback, deriveParentLoopState/queryGateAction logic, and renderer scrubbing/truncation/locales.
Tests — Export & Prompt Effects
packages/opencode/test/session/export.test.ts, packages/opencode/test/session/prompt-effect.test.ts
Adds tests for snapshot diagnostics selection (latest end wins; stop supersedes block) and updates prompt-effect tests to assert synthetic block+stop parts, final assistant stop text, and updated recovery semantics.
Tests — Harness Compatibility
packages/opencode/test/session/compaction.test.ts
Updates fake session processor in tests to satisfy expanded Handle interface with no-op/empty implementations for new loop methods.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Prompt as Prompt / Gate
    participant Diagnostics as SessionDiagnostics
    participant Processor as Session Processor
    participant Renderer as LoopRenderer

    Client->>Prompt: request tool call (tool,args,parentID,toolCallId)
    Prompt->>Diagnostics: applyLoopGate(tool,args,parentID)
    Diagnostics->>Processor: errorRecords(parentID), syntheticBlockSigKeys(parentID)
    Processor-->>Diagnostics: ToolErrorRecord[] + synthetic keys
    Diagnostics->>Diagnostics: deriveParentLoopState(records, syntheticKeys)
    Diagnostics->>Diagnostics: queryGateAction(parentLoopState, tool, inputHash, targetHash)
    alt observe
        Diagnostics-->>Prompt: decision: observe
        Prompt->>Processor: execute tool normally
        Processor-->>Prompt: tool result
        Prompt-->>Client: return result
    else block
        Diagnostics->>Processor: recordSyntheticBlock({...})
        Processor-->>Diagnostics: persisted block
        Prompt-->>Client: throw BlockedLoopError
    else stop
        Diagnostics->>Renderer: render({tool,state})
        Renderer-->>Diagnostics: renderedText
        Diagnostics->>Processor: recordSyntheticStop({... renderedText ...})
        Processor-->>Diagnostics: persisted stop + assistant text
        Prompt-->>Client: throw LoopStopError
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐇 I count the circles, inputs, and the errs,
I tuck away repeats and mark what blurs.
When probes keep circling, I raise a stop,
I trim the URLs and scrub each secret drop.
Hop onward wiser, with fewer spurious hops.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Title 'feat(session): low-yield repeated probing detection' accurately summarizes the main feature adding a pre-execution gate to detect and interrupt repeated tool failures within a user turn.
Description check ✅ Passed PR description fully covers all template sections: clear summary of changes, well-articulated problem/motivation, linked issue with spec reference, comprehensive verification steps with test scope, appropriate N/A for screenshots, and completed checklist.
Linked Issues check ✅ Passed Implementation meets all primary objectives from Issue #229: target-level cross-tool-family detection via signature hashing, persistent suspicion state tracked in loop metadata, escalation path (reminder→block→stop), scrubbed error summaries, and lightweight local-first design verified by comprehensive test coverage.
Out of Scope Changes check ✅ Passed All changes directly support Issue #229 objectives: loop-detection logic, gate decisions, synthetic block/stop synthesis, error scrubbing, snapshots, and test coverage. No unrelated refactoring, cosmetic changes, or scope creep detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/issue-229-loop-detection-v1

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request implements a "Loop Gate" mechanism designed to detect and prevent infinite loops during tool execution. It introduces signature-based tracking for tool inputs and targets, enabling the system to issue reminders, block repetitive failing calls, and ultimately halt execution with localized user feedback when a loop is detected. The changes include new diagnostic types, a LoopRenderer for Chinese messaging, and integration into the SessionProcessor and SessionPrompt workflows. I have no feedback to provide.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/opencode/src/session/diagnostics.ts`:
- Around line 255-279: The target-level loop detection is incorrectly limited to
a single tool because the prefiltered `real` array and target signature keys
include `r.tool`/`input.tool`; update the logic so that only input signatures
remain tool-scoped while target signatures and their matching ignore tool to
accumulate failures across tools. Concretely, stop filtering `real` by `r.tool
=== input.tool` (keep parentID and loopAction checks), change the target
signature key from `target:${input.tool}:${targetHash}` to
`target:${targetHash}` and change its matcher to compare only `r.targetHash ===
targetHash` (no tool check). Apply the same pattern in the other occurrences
referenced (the blocks around the other candidates at the later ranges: 336-346
and 389-391) so target counting is global across tools under the same `parentID`
while input counting remains tool-scoped.

In `@packages/opencode/src/session/export.ts`:
- Around line 172-199: The DFS currently overwrites `last` with whichever
blocked probe is visited last in `walk`, causing older blocks to replace newer
ones; modify the `walk` function to track the most recent block timestamp (e.g.,
extract a numeric timestamp from `part.state.timestamp` or
`part.state.metadata.timestamp` or fallback to 0) and only set/replace `last`
when the current part's timestamp is greater than the stored `lastTimestamp`;
add a `let lastTimestamp = 0` outside `walk`, update it whenever you assign
`last`, and keep the rest of the `last` object fields (`parentID`, `type`,
`action`, `tool`, `completedFailures`) the same so `diagnostics.loop.last`
reflects the latest blocked probe by timestamp rather than DFS order.

In `@packages/opencode/src/session/loop-renderer.ts`:
- Around line 14-46: Don't echo raw state.lastInput into rendered stop messages;
instead use scrubbed summaries. Replace any interpolations of state.lastInput
(including the branches noted around the webfetch block and the generic-tool
branches) with safe summaries produced by extractURL +
SessionDiagnostics.truncateForRenderer (use truncatedURL) or
describeGenericTarget (use targetSummary), and if neither yields a value fall
back to a fixed redacted placeholder like "[redacted target]"; ensure you only
interpolate truncatedURL or targetSummary (and truncatedError) in the returned
strings and remove any direct uses of state.lastInput.

In `@packages/opencode/src/session/prompt.ts`:
- Around line 97-100: The current branch returns an "observe" GateOutcome when
processor.hasStopped(parentID) is true, which can reopen the gate after
recordSyntheticStop(...) fired; replace that return to short-circuit with a
non-recording stop GateOutcome (not "observe") so the gate stays closed—for
example, change the processor.hasStopped(parentID) branch to return a stop
outcome that indicates "do not record" (e.g., { kind: "stop", recorded: false }
satisfies GateOutcome) instead of { kind: "observe" }, referencing
processor.hasStopped, recordSyntheticStop, and the GateOutcome type.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: b85d80d7-7f48-4a89-a636-43f40e6ceda9

📥 Commits

Reviewing files that changed from the base of the PR and between 99f98ff and 39d22a3.

📒 Files selected for processing (11)
  • packages/opencode/src/session/diagnostics.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/src/session/processor.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/test/session/loop-gate.test.ts
  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: unit-windows-app
  • GitHub Check: unit-windows-desktop
  • GitHub Check: unit-windows-opencode-session
  • GitHub Check: unit-windows-opencode-server-tools
  • GitHub Check: unit-windows-opencode-config-project
  • GitHub Check: unit-opencode
  • GitHub Check: unit-desktop
  • GitHub Check: smoke-macos-arm64
  • GitHub Check: e2e-artifacts
  • GitHub Check: analyze-js-ts
🧰 Additional context used
📓 Path-based instructions (2)
packages/opencode/**/*.ts

📄 CodeRabbit inference engine (packages/opencode/AGENTS.md)

packages/opencode/**/*.ts: Use Effect.gen(function* () { ... }) for Effect composition
Use Effect.fn("Domain.method") for named/traced effects and Effect.fnUntraced for internal helpers; these accept pipeable operators as extra arguments to avoid unnecessary outer .pipe() wrappers
Use Effect.callback for callback-based APIs
Prefer DateTime.nowAsDate over new Date(yield* Clock.currentTimeMillis) when you need a Date in Effect code
Use Schema.Class for multi-field data in Effect schemas
Use branded schemas (Schema.brand) for single-value types in Effect
Use Schema.TaggedErrorClass for typed errors in Effect schemas
Use Schema.Defect instead of unknown for defect-like causes in Effect code
In Effect.gen / Effect.fn, prefer yield* new MyError(...) over yield* Effect.fail(new MyError(...)) for direct early-failure branches
Use makeRuntime from src/effect/run-service.ts for all services; it returns { runPromise, runFork, runCallback } backed by a shared memoMap that deduplicates layers
Use InstanceState from src/effect/instance-state.ts for per-directory or per-project state that needs per-instance cleanup; do work directly in the InstanceState.make closure where ScopedCache handles run-once semantics
Use Effect.addFinalizer or Effect.acquireRelease inside the InstanceState.make closure for cleanup (subscriptions, process teardown, etc.)
Use Effect.forkScoped inside the InstanceState.make closure for background stream consumers — the fiber is interrupted when the instance is disposed
Prefer FileSystem.FileSystem instead of raw fs/promises for effectful file I/O in Effect services
Prefer ChildProcessSpawner.ChildProcessSpawner with ChildProcess.make(...) instead of custom process wrappers in Effect services
Prefer HttpClient.HttpClient instead of raw fetch in Effect services
Prefer Path.Path, Config, Clock, and DateTime services when those concerns are already inside Effect code
For backgroun...

Files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/test/session/loop-gate.test.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/src/session/diagnostics.ts
  • packages/opencode/src/session/processor.ts
packages/opencode/test/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (packages/opencode/test/AGENTS.md)

packages/opencode/test/**/*.test.{ts,tsx}: Use the tmpdir function from fixture/fixture.ts to create temporary directories for tests with automatic cleanup. Use await using syntax to ensure automatic cleanup when the variable goes out of scope.
When using the tmpdir function with git repository support, pass the git: true option to initialize a git repo with a root commit.
Use the config option in tmpdir to write an opencode.json config file during test setup by passing a partial Config.Info object.
Use the init option in tmpdir to define custom setup functions that can return extra data accessible via tmp.extra, and use the dispose option for custom cleanup logic.
Use testEffect(...) from test/lib/effect.ts for tests that exercise Effect services or Effect-based workflows.
Use it.effect(...) when the test should run with TestClock and TestConsole. Use it.live(...) when the test depends on real time, filesystem mtimes, child processes, git, locks, or other live OS behavior.
Prefer Effect-aware helpers from fixture/fixture.ts over building manual runtimes in tests: use tmpdirScoped() for scoped temp directories, provideInstance(dir)(effect) for low-level binding without directory creation, provideTmpdirInstance(...) for single temp instance binding, or provideTmpdirServer(...) for tests that also need the test LLM server.
Define const it = testEffect(...) near the top of the test file and keep the test body inside Effect.gen(function* () { ... }). Yield services directly with yield* MyService.Service or yield* MyTool.
Avoid custom ManagedRuntime, attach(...), or ad hoc run(...) wrappers in Effect tests when testEffect(...) already provides the runtime.
When a test needs instance-local state, prefer provideTmpdirInstance(...) or provideInstance(...) over manual Instance.provide(...) inside Promise-style tests.

Files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/test/session/loop-gate.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
🧠 Learnings (21)
📚 Learning: 2026-04-20T14:36:04.113Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/app/e2e/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:04.113Z
Learning: Applies to packages/app/e2e/**/*.spec.ts : Use fixture-managed cleanup with `withSession(sdk, title, callback)` for temporary sessions instead of calling `sdk.session.delete(...)` directly

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
📚 Learning: 2026-04-24T03:51:54.050Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 206
File: packages/app/e2e/prompt/prompt-footer-focus.spec.ts:131-143
Timestamp: 2026-04-24T03:51:54.050Z
Learning: In Astro-Han/pawwork E2E tests (packages/app/e2e/fixtures.ts), `project.prompt(text)` internally calls `trackSession(next.sessionID, active.directory)` after the prompt submission is observed and the active session is resolved. Tests that obtain a `sessionID` from `project.prompt()` do NOT need to call `project.trackSession(sessionID)` manually — it is already registered for teardown. Calling it again would be duplicate ownership.

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/test/session/loop-gate.test.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/src/session/processor.ts
📚 Learning: 2026-04-20T14:36:04.113Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/app/e2e/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:04.113Z
Learning: Applies to packages/app/e2e/**/*.spec.ts : Call `project.trackSession(sessionID, directory?)` and `project.trackDirectory(directory)` for any resources created outside the fixture so teardown can clean them up

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
📚 Learning: 2026-04-24T00:02:53.315Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 203
File: packages/app/e2e/sidebar/sidebar-session-links.spec.ts:34-55
Timestamp: 2026-04-24T00:02:53.315Z
Learning: In Astro-Han/pawwork E2E tests (`packages/app/e2e/**/*.spec.ts`), `project.trackDirectory()` and `project.trackSession()` cannot be called before `project.open()` — the `project` fixture throws until `open()` initializes its internal state. The correct pattern is: call `project.trackSession(sessionID)` from inside the `beforeGoto` callback (where state already exists), call `project.trackDirectory(directory)` and cross-workspace `project.trackSession(id, directory)` immediately after `project.open()` returns, and rely on explicit `finally` cleanup (e.g. `cleanupSession` / `cleanupTestProject`) for any resources created before `open()` that cannot yet be tracked via the fixture.

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Prefer Effect-aware helpers from `fixture/fixture.ts` over building manual runtimes in tests: use `tmpdirScoped()` for scoped temp directories, `provideInstance(dir)(effect)` for low-level binding without directory creation, `provideTmpdirInstance(...)` for single temp instance binding, or `provideTmpdirServer(...)` for tests that also need the test LLM server.

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Define `const it = testEffect(...)` near the top of the test file and keep the test body inside `Effect.gen(function* () { ... })`. Yield services directly with `yield* MyService.Service` or `yield* MyTool`.

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/test/session/loop-gate.test.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/src/session/processor.ts
📚 Learning: 2026-04-23T08:51:00.819Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 186
File: packages/opencode/test/plugin/workspace-adaptor.test.ts:139-144
Timestamp: 2026-04-23T08:51:00.819Z
Learning: For pawwork tests under packages/opencode/test/**, auth.json teardown may intentionally combine `Filesystem.write` (from `packages/opencode/src/util/filesystem.ts`) with `node:fs/promises` `unlink` for cleanup. Do not flag this as inconsistent style; it is the established/intentional pattern because `Filesystem` does not provide a `remove`/`unlink` helper.

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/test/session/loop-gate.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
📚 Learning: 2026-04-22T09:32:54.556Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 126
File: packages/opencode/test/provider/provider.test.ts:64-85
Timestamp: 2026-04-22T09:32:54.556Z
Learning: In `packages/opencode/test/provider/provider.test.ts`, the file intentionally uses AppRuntime-based async helpers (`run`, `list`, `getProvider`, etc.) rather than `testEffect(...)` for all tests. Converting individual tests to `testEffect` while leaving the rest on the async pattern would create internal inconsistency. A full harness migration of this file is the right approach if the pattern needs to change, but that should be a separate PR.

Applied to files:

  • packages/opencode/test/session/loop-renderer.test.ts
📚 Learning: 2026-04-22T08:49:47.800Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 126
File: packages/desktop-electron/src/main/index-sidecar-source.test.ts:3-11
Timestamp: 2026-04-22T08:49:47.800Z
Learning: In `packages/desktop-electron/src/main/index-sidecar-source.test.ts` (Astro-Han/pawwork), the test intentionally uses `expect(source).toContain` / `expect(source).not.toContain` string matching against the raw `index.ts` source text as a lightweight sidecar contract guard. The maintainer has explicitly chosen not to introduce an AST parser (e.g., `babel/parser` or acorn) for this purpose. Do not flag these string-based assertions as fragile or suggest converting them to AST-based matching.

Applied to files:

  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts
📚 Learning: 2026-04-20T14:36:04.113Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/app/e2e/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:04.113Z
Learning: Applies to packages/app/e2e/**/*.spec.ts : Test one feature per test file

Applied to files:

  • packages/opencode/test/session/loop-renderer.test.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Use `testEffect(...)` from `test/lib/effect.ts` for tests that exercise Effect services or Effect-based workflows.

Applied to files:

  • packages/opencode/test/session/prompt-effect.test.ts
📚 Learning: 2026-04-24T13:03:10.835Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 222
File: packages/desktop-electron/src/main/index.ts:686-692
Timestamp: 2026-04-24T13:03:10.835Z
Learning: In `packages/desktop-electron/src/main/index.ts`, the `checkForUpdates()` function intentionally uses recursive self-invocation for the "Retry" path in the update-check failure dialog. This is mandated by the v5.2 design spec (`#213`): "Await the retry recursion and log any rejection so support can see repeated failures." Because retries are user-paced (require a button click), all prior async frames have already unwound through microtasks before the next attempt, so there is no stack/frame-nesting problem in practice. Do not suggest refactoring this to an iterative loop.

Applied to files:

  • packages/opencode/test/session/prompt-effect.test.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Avoid custom `ManagedRuntime`, `attach(...)`, or ad hoc `run(...)` wrappers in Effect tests when `testEffect(...)` already provides the runtime.

Applied to files:

  • packages/opencode/test/session/prompt-effect.test.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Use `it.effect(...)` when the test should run with `TestClock` and `TestConsole`. Use `it.live(...)` when the test depends on real time, filesystem mtimes, child processes, git, locks, or other live OS behavior.

Applied to files:

  • packages/opencode/test/session/prompt-effect.test.ts
📚 Learning: 2026-04-26T16:34:54.895Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 247
File: packages/ui/src/components/message-part.tsx:1322-1324
Timestamp: 2026-04-26T16:34:54.895Z
Learning: In Astro-Han/pawwork (`packages/ui/src/components/message-part.tsx`), the `taskId` createMemo and `childSessionId` createMemo both intentionally read only from `partMetadata().sessionId` (populated post-execution), not from `input.task_id` / `input.subagent_session_id`. This has always been the case — the original code never read the input field either. Adding an `input.subagent_session_id` fallback would be a new capability, not a bug fix. Do NOT flag the absence of this fallback as a regression in PR `#247` or future PRs unless there is a concrete case where metadata is not populated.

Applied to files:

  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/src/session/processor.ts
📚 Learning: 2026-04-25T12:52:47.074Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 234
File: packages/opencode/src/share/session.ts:27-27
Timestamp: 2026-04-25T12:52:47.074Z
Learning: In `packages/opencode/src/share/session.ts`, the local `ensureEnabled` closure is intentionally duplicated from `ShareRuntime.ensureEnabled` in `runtime.ts`. Using the shared `ShareRuntime.ensureEnabled` effect directly would leak `CloudShareGate` back into the `R` (requirement) type of `share`, `unshare`, and `create`, because the shared effect re-yields the service rather than capturing an already-resolved gate reference. The current pattern resolves `gate` once at the top of the outer `Effect.gen` and then closes over it in a local `ensureEnabled`, keeping the inner effects' requirement types clean. A comment in the file points to `runtime.ts` to make the intentional duplication discoverable. The real fix would be refactoring `ensureEnabled` to accept a gate parameter, but that change is larger than the DRY benefit warrants. Do not flag this duplication as a maintenance issue.

Applied to files:

  • packages/opencode/test/session/loop-gate.test.ts
  • packages/opencode/src/session/prompt.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : In `Effect.gen` / `Effect.fn`, prefer `yield* new MyError(...)` over `yield* Effect.fail(new MyError(...))` for direct early-failure branches

Applied to files:

  • packages/opencode/src/session/prompt.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : For background loops or scheduled tasks, use `Effect.repeat` or `Effect.schedule` with `Effect.forkScoped` in the layer definition

Applied to files:

  • packages/opencode/src/session/prompt.ts
📚 Learning: 2026-04-25T12:52:32.462Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 234
File: packages/desktop-electron/src/main/ipc.ts:238-263
Timestamp: 2026-04-25T12:52:32.462Z
Learning: In Astro-Han/pawwork (`packages/desktop-electron/src/main/ipc.ts`), `deps.getServerReadyData()` (backed by `serverReady.promise` in `index.ts`) resolves once at server startup and remains settled; it is not expected to reject in practice. Do not flag the absence of a try-catch around it in the `export-session` IPC handler — the network/fetch layer in `server-client.ts` already has a 10-second AbortController timeout and returns a typed `{ok: false, error}` payload, covering the real failure modes.

Applied to files:

  • packages/opencode/src/session/prompt.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : Prefer `Path.Path`, `Config`, `Clock`, and `DateTime` services when those concerns are already inside Effect code

Applied to files:

  • packages/opencode/src/session/prompt.ts
📚 Learning: 2026-04-20T14:36:04.113Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/app/e2e/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:04.113Z
Learning: Applies to packages/app/e2e/packages/app/src/testing/**/*.ts : Test-only hooks must be inert unless explicitly enabled and should not add normal-runtime listeners, reactive subscriptions, or per-update allocations

Applied to files:

  • packages/opencode/test/session/diagnostics.test.ts

Comment thread packages/opencode/src/session/diagnostics.ts
Comment thread packages/opencode/src/session/export.ts
Comment thread packages/opencode/src/session/loop-renderer.ts Outdated
Comment thread packages/opencode/src/session/prompt.ts

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Nitpicky pass focused on loop-gate correctness and spec alignment.

Comment thread packages/opencode/src/session/prompt.ts Outdated
Comment thread packages/opencode/src/session/processor.ts
Comment thread packages/opencode/src/session/diagnostics.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
packages/opencode/test/session/prompt-effect.test.ts (1)

498-562: ⚠️ Potential issue | 🟡 Minor

Add a regression case for the post-stop re-entry guard.

This sequence proves the first synthetic stop, but it never hits the processor.hasStopped(parentID) branch in packages/opencode/src/session/prompt.ts. An 8th queued tool call is still needed here to catch regressions where auto-resume reopens the gate after the stop.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/opencode/test/session/prompt-effect.test.ts` around lines 498 - 562,
The test "loop gate blocks then stops after autoResume budget on repeated tool
errors" currently queues 7 identical tool calls then sends "done", which doesn't
exercise the processor.hasStopped(parentID) re-entry guard; add one more queued
tool invocation (another yield* llm.tool("read", input) so there are 8 identical
calls) before the yield* llm.text("done") to trigger the post-stop re-entry
branch and catch regressions where auto-resume could reopen the gate after stop.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@packages/opencode/test/session/prompt-effect.test.ts`:
- Around line 498-562: The test "loop gate blocks then stops after autoResume
budget on repeated tool errors" currently queues 7 identical tool calls then
sends "done", which doesn't exercise the processor.hasStopped(parentID) re-entry
guard; add one more queued tool invocation (another yield* llm.tool("read",
input) so there are 8 identical calls) before the yield* llm.text("done") to
trigger the post-stop re-entry branch and catch regressions where auto-resume
could reopen the gate after stop.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 82089f3d-0a1b-497f-89b6-961acee8b6c3

📥 Commits

Reviewing files that changed from the base of the PR and between 39d22a3 and d04b7a4.

📒 Files selected for processing (8)
  • packages/opencode/src/session/diagnostics.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (12)
  • GitHub Check: unit-windows-app
  • GitHub Check: unit-opencode
  • GitHub Check: unit-windows-desktop
  • GitHub Check: typecheck
  • GitHub Check: unit-windows-opencode-server-tools
  • GitHub Check: unit-windows-opencode-session
  • GitHub Check: unit-windows-opencode-config-project
  • GitHub Check: unit-app
  • GitHub Check: unit-desktop
  • GitHub Check: smoke-macos-arm64
  • GitHub Check: e2e-artifacts
  • GitHub Check: analyze-js-ts
🧰 Additional context used
📓 Path-based instructions (2)
packages/opencode/**/*.ts

📄 CodeRabbit inference engine (packages/opencode/AGENTS.md)

packages/opencode/**/*.ts: Use Effect.gen(function* () { ... }) for Effect composition
Use Effect.fn("Domain.method") for named/traced effects and Effect.fnUntraced for internal helpers; these accept pipeable operators as extra arguments to avoid unnecessary outer .pipe() wrappers
Use Effect.callback for callback-based APIs
Prefer DateTime.nowAsDate over new Date(yield* Clock.currentTimeMillis) when you need a Date in Effect code
Use Schema.Class for multi-field data in Effect schemas
Use branded schemas (Schema.brand) for single-value types in Effect
Use Schema.TaggedErrorClass for typed errors in Effect schemas
Use Schema.Defect instead of unknown for defect-like causes in Effect code
In Effect.gen / Effect.fn, prefer yield* new MyError(...) over yield* Effect.fail(new MyError(...)) for direct early-failure branches
Use makeRuntime from src/effect/run-service.ts for all services; it returns { runPromise, runFork, runCallback } backed by a shared memoMap that deduplicates layers
Use InstanceState from src/effect/instance-state.ts for per-directory or per-project state that needs per-instance cleanup; do work directly in the InstanceState.make closure where ScopedCache handles run-once semantics
Use Effect.addFinalizer or Effect.acquireRelease inside the InstanceState.make closure for cleanup (subscriptions, process teardown, etc.)
Use Effect.forkScoped inside the InstanceState.make closure for background stream consumers — the fiber is interrupted when the instance is disposed
Prefer FileSystem.FileSystem instead of raw fs/promises for effectful file I/O in Effect services
Prefer ChildProcessSpawner.ChildProcessSpawner with ChildProcess.make(...) instead of custom process wrappers in Effect services
Prefer HttpClient.HttpClient instead of raw fetch in Effect services
Prefer Path.Path, Config, Clock, and DateTime services when those concerns are already inside Effect code
For backgroun...

Files:

  • packages/opencode/test/session/export.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/src/session/diagnostics.ts
packages/opencode/test/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (packages/opencode/test/AGENTS.md)

packages/opencode/test/**/*.test.{ts,tsx}: Use the tmpdir function from fixture/fixture.ts to create temporary directories for tests with automatic cleanup. Use await using syntax to ensure automatic cleanup when the variable goes out of scope.
When using the tmpdir function with git repository support, pass the git: true option to initialize a git repo with a root commit.
Use the config option in tmpdir to write an opencode.json config file during test setup by passing a partial Config.Info object.
Use the init option in tmpdir to define custom setup functions that can return extra data accessible via tmp.extra, and use the dispose option for custom cleanup logic.
Use testEffect(...) from test/lib/effect.ts for tests that exercise Effect services or Effect-based workflows.
Use it.effect(...) when the test should run with TestClock and TestConsole. Use it.live(...) when the test depends on real time, filesystem mtimes, child processes, git, locks, or other live OS behavior.
Prefer Effect-aware helpers from fixture/fixture.ts over building manual runtimes in tests: use tmpdirScoped() for scoped temp directories, provideInstance(dir)(effect) for low-level binding without directory creation, provideTmpdirInstance(...) for single temp instance binding, or provideTmpdirServer(...) for tests that also need the test LLM server.
Define const it = testEffect(...) near the top of the test file and keep the test body inside Effect.gen(function* () { ... }). Yield services directly with yield* MyService.Service or yield* MyTool.
Avoid custom ManagedRuntime, attach(...), or ad hoc run(...) wrappers in Effect tests when testEffect(...) already provides the runtime.
When a test needs instance-local state, prefer provideTmpdirInstance(...) or provideInstance(...) over manual Instance.provide(...) inside Promise-style tests.

Files:

  • packages/opencode/test/session/export.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
🧠 Learnings (24)
📓 Common learnings
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 264
File: packages/opencode/src/session/diagnostics.ts:255-279
Timestamp: 2026-04-27T08:27:40.791Z
Learning: In `packages/opencode/src/session/diagnostics.ts` (PR `#264`, issue `#229`), target-level loop detection is intentionally tool-scoped. The `real` filter includes `r.tool === input.tool` and target signature keys are formatted as `target:${tool}:${targetHash}`. Cross-tool retries on the same URL/path/query are considered legitimate exploration (e.g., webfetch failed → switch to a different tool), NOT a stuck loop. Only same-tool + same-target accumulation counts toward block/stop. Do NOT suggest removing the tool scope from target signature keys or the `real` filter.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 234
File: packages/opencode/src/share/session.ts:27-27
Timestamp: 2026-04-25T12:52:47.074Z
Learning: In `packages/opencode/src/share/session.ts`, the local `ensureEnabled` closure is intentionally duplicated from `ShareRuntime.ensureEnabled` in `runtime.ts`. Using the shared `ShareRuntime.ensureEnabled` effect directly would leak `CloudShareGate` back into the `R` (requirement) type of `share`, `unshare`, and `create`, because the shared effect re-yields the service rather than capturing an already-resolved gate reference. The current pattern resolves `gate` once at the top of the outer `Effect.gen` and then closes over it in a local `ensureEnabled`, keeping the inner effects' requirement types clean. A comment in the file points to `runtime.ts` to make the intentional duplication discoverable. The real fix would be refactoring `ensureEnabled` to accept a gate parameter, but that change is larger than the DRY benefit warrants. Do not flag this duplication as a maintenance issue.
📚 Learning: 2026-04-27T08:27:40.791Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 264
File: packages/opencode/src/session/diagnostics.ts:255-279
Timestamp: 2026-04-27T08:27:40.791Z
Learning: In `packages/opencode/src/session/diagnostics.ts` (PR `#264`, issue `#229`), target-level loop detection is intentionally tool-scoped. The `real` filter includes `r.tool === input.tool` and target signature keys are formatted as `target:${tool}:${targetHash}`. Cross-tool retries on the same URL/path/query are considered legitimate exploration (e.g., webfetch failed → switch to a different tool), NOT a stuck loop. Only same-tool + same-target accumulation counts toward block/stop. Do NOT suggest removing the tool scope from target signature keys or the `real` filter.

Applied to files:

  • packages/opencode/test/session/export.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Define `const it = testEffect(...)` near the top of the test file and keep the test body inside `Effect.gen(function* () { ... })`. Yield services directly with `yield* MyService.Service` or `yield* MyTool`.

Applied to files:

  • packages/opencode/test/session/export.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Prefer Effect-aware helpers from `fixture/fixture.ts` over building manual runtimes in tests: use `tmpdirScoped()` for scoped temp directories, `provideInstance(dir)(effect)` for low-level binding without directory creation, `provideTmpdirInstance(...)` for single temp instance binding, or `provideTmpdirServer(...)` for tests that also need the test LLM server.

Applied to files:

  • packages/opencode/test/session/export.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
📚 Learning: 2026-04-20T14:36:04.113Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/app/e2e/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:04.113Z
Learning: Applies to packages/app/e2e/**/*.spec.ts : Call `project.trackSession(sessionID, directory?)` and `project.trackDirectory(directory)` for any resources created outside the fixture so teardown can clean them up

Applied to files:

  • packages/opencode/test/session/export.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Use the `tmpdir` function from `fixture/fixture.ts` to create temporary directories for tests with automatic cleanup. Use `await using` syntax to ensure automatic cleanup when the variable goes out of scope.

Applied to files:

  • packages/opencode/test/session/export.test.ts
📚 Learning: 2026-04-23T08:51:00.819Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 186
File: packages/opencode/test/plugin/workspace-adaptor.test.ts:139-144
Timestamp: 2026-04-23T08:51:00.819Z
Learning: For pawwork tests under packages/opencode/test/**, auth.json teardown may intentionally combine `Filesystem.write` (from `packages/opencode/src/util/filesystem.ts`) with `node:fs/promises` `unlink` for cleanup. Do not flag this as inconsistent style; it is the established/intentional pattern because `Filesystem` does not provide a `remove`/`unlink` helper.

Applied to files:

  • packages/opencode/test/session/export.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
📚 Learning: 2026-04-24T03:51:54.050Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 206
File: packages/app/e2e/prompt/prompt-footer-focus.spec.ts:131-143
Timestamp: 2026-04-24T03:51:54.050Z
Learning: In Astro-Han/pawwork E2E tests (packages/app/e2e/fixtures.ts), `project.prompt(text)` internally calls `trackSession(next.sessionID, active.directory)` after the prompt submission is observed and the active session is resolved. Tests that obtain a `sessionID` from `project.prompt()` do NOT need to call `project.trackSession(sessionID)` manually — it is already registered for teardown. Calling it again would be duplicate ownership.

Applied to files:

  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/diagnostics.test.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Use `testEffect(...)` from `test/lib/effect.ts` for tests that exercise Effect services or Effect-based workflows.

Applied to files:

  • packages/opencode/test/session/prompt-effect.test.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Avoid custom `ManagedRuntime`, `attach(...)`, or ad hoc `run(...)` wrappers in Effect tests when `testEffect(...)` already provides the runtime.

Applied to files:

  • packages/opencode/test/session/prompt-effect.test.ts
📚 Learning: 2026-04-24T13:03:10.835Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 222
File: packages/desktop-electron/src/main/index.ts:686-692
Timestamp: 2026-04-24T13:03:10.835Z
Learning: In `packages/desktop-electron/src/main/index.ts`, the `checkForUpdates()` function intentionally uses recursive self-invocation for the "Retry" path in the update-check failure dialog. This is mandated by the v5.2 design spec (`#213`): "Await the retry recursion and log any rejection so support can see repeated failures." Because retries are user-paced (require a button click), all prior async frames have already unwound through microtasks before the next attempt, so there is no stack/frame-nesting problem in practice. Do not suggest refactoring this to an iterative loop.

Applied to files:

  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Use `it.effect(...)` when the test should run with `TestClock` and `TestConsole`. Use `it.live(...)` when the test depends on real time, filesystem mtimes, child processes, git, locks, or other live OS behavior.

Applied to files:

  • packages/opencode/test/session/prompt-effect.test.ts
📚 Learning: 2026-04-20T14:36:04.113Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/app/e2e/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:04.113Z
Learning: Applies to packages/app/e2e/packages/app/src/testing/**/*.ts : Test-only hooks must be inert unless explicitly enabled and should not add normal-runtime listeners, reactive subscriptions, or per-update allocations

Applied to files:

  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
📚 Learning: 2026-04-22T09:32:54.556Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 126
File: packages/opencode/test/provider/provider.test.ts:64-85
Timestamp: 2026-04-22T09:32:54.556Z
Learning: In `packages/opencode/test/provider/provider.test.ts`, the file intentionally uses AppRuntime-based async helpers (`run`, `list`, `getProvider`, etc.) rather than `testEffect(...)` for all tests. Converting individual tests to `testEffect` while leaving the rest on the async pattern would create internal inconsistency. A full harness migration of this file is the right approach if the pattern needs to change, but that should be a separate PR.

Applied to files:

  • packages/opencode/test/session/prompt-effect.test.ts
📚 Learning: 2026-04-23T15:25:31.118Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 193
File: packages/app/e2e/sidebar/sidebar-leading-slot.spec.ts:5-55
Timestamp: 2026-04-23T15:25:31.118Z
Learning: In Astro-Han/pawwork E2E tests, reaching a real "running" session state requires the `project` fixture (for model bootstrap) plus `llm.wait(1)` orchestration. The bare `sdk` fixture used by `withSession` does not trigger an LLM call even with `agent: "build"` + `system` prompt set via `sdk.session.promptAsync`, so simulating running state is not lightweight in the current test infrastructure.

Applied to files:

  • packages/opencode/test/session/prompt-effect.test.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : In `Effect.gen` / `Effect.fn`, prefer `yield* new MyError(...)` over `yield* Effect.fail(new MyError(...))` for direct early-failure branches

Applied to files:

  • packages/opencode/src/session/prompt.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : For background loops or scheduled tasks, use `Effect.repeat` or `Effect.schedule` with `Effect.forkScoped` in the layer definition

Applied to files:

  • packages/opencode/src/session/prompt.ts
📚 Learning: 2026-04-25T12:52:47.074Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 234
File: packages/opencode/src/share/session.ts:27-27
Timestamp: 2026-04-25T12:52:47.074Z
Learning: In `packages/opencode/src/share/session.ts`, the local `ensureEnabled` closure is intentionally duplicated from `ShareRuntime.ensureEnabled` in `runtime.ts`. Using the shared `ShareRuntime.ensureEnabled` effect directly would leak `CloudShareGate` back into the `R` (requirement) type of `share`, `unshare`, and `create`, because the shared effect re-yields the service rather than capturing an already-resolved gate reference. The current pattern resolves `gate` once at the top of the outer `Effect.gen` and then closes over it in a local `ensureEnabled`, keeping the inner effects' requirement types clean. A comment in the file points to `runtime.ts` to make the intentional duplication discoverable. The real fix would be refactoring `ensureEnabled` to accept a gate parameter, but that change is larger than the DRY benefit warrants. Do not flag this duplication as a maintenance issue.

Applied to files:

  • packages/opencode/src/session/prompt.ts
📚 Learning: 2026-04-25T12:52:32.462Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 234
File: packages/desktop-electron/src/main/ipc.ts:238-263
Timestamp: 2026-04-25T12:52:32.462Z
Learning: In Astro-Han/pawwork (`packages/desktop-electron/src/main/ipc.ts`), `deps.getServerReadyData()` (backed by `serverReady.promise` in `index.ts`) resolves once at server startup and remains settled; it is not expected to reject in practice. Do not flag the absence of a try-catch around it in the `export-session` IPC handler — the network/fetch layer in `server-client.ts` already has a 10-second AbortController timeout and returns a typed `{ok: false, error}` payload, covering the real failure modes.

Applied to files:

  • packages/opencode/src/session/prompt.ts
📚 Learning: 2026-04-26T16:34:54.895Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 247
File: packages/ui/src/components/message-part.tsx:1322-1324
Timestamp: 2026-04-26T16:34:54.895Z
Learning: In Astro-Han/pawwork (`packages/ui/src/components/message-part.tsx`), the `taskId` createMemo and `childSessionId` createMemo both intentionally read only from `partMetadata().sessionId` (populated post-execution), not from `input.task_id` / `input.subagent_session_id`. This has always been the case — the original code never read the input field either. Adding an `input.subagent_session_id` fallback would be a new capability, not a bug fix. Do NOT flag the absence of this fallback as a regression in PR `#247` or future PRs unless there is a concrete case where metadata is not populated.

Applied to files:

  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-24T00:02:53.315Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 203
File: packages/app/e2e/sidebar/sidebar-session-links.spec.ts:34-55
Timestamp: 2026-04-24T00:02:53.315Z
Learning: In Astro-Han/pawwork E2E tests (`packages/app/e2e/**/*.spec.ts`), `project.trackDirectory()` and `project.trackSession()` cannot be called before `project.open()` — the `project` fixture throws until `open()` initializes its internal state. The correct pattern is: call `project.trackSession(sessionID)` from inside the `beforeGoto` callback (where state already exists), call `project.trackDirectory(directory)` and cross-workspace `project.trackSession(id, directory)` immediately after `project.open()` returns, and rely on explicit `finally` cleanup (e.g. `cleanupSession` / `cleanupTestProject`) for any resources created before `open()` that cannot yet be tracked via the fixture.

Applied to files:

  • packages/opencode/test/session/diagnostics.test.ts
📚 Learning: 2026-04-22T08:49:47.800Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 126
File: packages/desktop-electron/src/main/index-sidecar-source.test.ts:3-11
Timestamp: 2026-04-22T08:49:47.800Z
Learning: In `packages/desktop-electron/src/main/index-sidecar-source.test.ts` (Astro-Han/pawwork), the test intentionally uses `expect(source).toContain` / `expect(source).not.toContain` string matching against the raw `index.ts` source text as a lightweight sidecar contract guard. The maintainer has explicitly chosen not to introduce an AST parser (e.g., `babel/parser` or acorn) for this purpose. Do not flag these string-based assertions as fragile or suggest converting them to AST-based matching.

Applied to files:

  • packages/opencode/test/session/diagnostics.test.ts
📚 Learning: 2026-04-20T14:36:04.113Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/app/e2e/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:04.113Z
Learning: Applies to packages/app/e2e/**/*.spec.ts : Use fixture-managed cleanup with `withSession(sdk, title, callback)` for temporary sessions instead of calling `sdk.session.delete(...)` directly

Applied to files:

  • packages/opencode/test/session/diagnostics.test.ts
📚 Learning: 2026-04-25T11:52:40.349Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 233
File: packages/desktop-electron/src/main/server.ts:60-60
Timestamp: 2026-04-25T11:52:40.349Z
Learning: In Astro-Han/pawwork (`packages/desktop-electron/src/main/index.ts`), `ensureLoopbackNoProxy()` (defined at line 551) is called inside `setupApp()` at line 263, which runs synchronously before `initialize()` (line 321) and `spawnLocalServer()` (line 395). It unconditionally upserts `127.0.0.1`, `localhost`, and `::1` into both `process.env.NO_PROXY` and `process.env.no_proxy`. This guarantees that `configureProxyDispatcher(process.env)` in `server.ts` always receives a `process.env` that already excludes loopback from proxying. Do not flag the absence of a loopback-merge inside `configureProxyDispatcher` or `normalizeProxyConfig` — the exclusion is already enforced at a higher layer before those functions are ever called.

Applied to files:

  • packages/opencode/src/session/diagnostics.ts
🪛 Betterleaks (1.1.2)
packages/opencode/test/session/diagnostics.test.ts

[high] 253-253: Discovered a potential authorization token provided in a curl command header, which could compromise the curl accessed resource.

(curl-auth-header)

🔇 Additional comments (10)
packages/opencode/src/session/prompt.ts (1)

99-108: Nice fix for the post-stop auto-resume path.

Keeping the gate closed once a synthetic stop is recorded prevents later ai-sdk resumptions from executing real tools or duplicating the halt summary.

packages/opencode/src/session/loop-renderer.ts (1)

14-68: The stop renderer now keeps the useful context without re-exposing raw inputs.

Stripping query strings/fragments for webfetch and falling back to tool/error-only summaries for other tools is the right tradeoff here.

packages/opencode/src/session/export.ts (1)

163-204: Timestamp-based selection for diagnostics.loop.last looks correct.

Choosing the latest synthetic block by part time instead of tree-walk order makes snapshot diagnostics stable across nested session trees.

packages/opencode/src/session/diagnostics.ts (7)

20-41: Well-designed type definitions for the gate system.

The discriminated union GateDecision cleanly captures the three gate outcomes, and SignatureState appropriately tracks the per-signature state needed for the escalation logic (reminder → block → stop).


102-122: Solid implementation of byte-safe truncation.

The function correctly handles:

  • UTF-8 multi-byte sequences by detecting the replacement character (0xFFFD) at the truncation boundary
  • Partial URL-encoded sequences that could produce malformed output

The fallback logic for non-string values (JSON.stringify(value) ?? String(value)) properly handles edge cases like undefined or functions.


130-134: Clean refactor exposing fallback status.

Making isFallback explicit allows callers to distinguish real targets from input-hash fallbacks, preventing false positive target-based blocking when no meaningful target could be extracted.


255-280: Tool-scoped filtering is intentionally correct per design.

The filter at line 258 (r.tool === input.tool) and the tool-prefixed signature keys (lines 269, 276) are intentional. Cross-tool retries are considered legitimate exploration. Based on learnings, only same-tool + same-target accumulation counts toward block/stop.


329-404: Gate logic correctly implements the escalation chain.

The implementation properly:

  • Aggregates failure counts per signature from real (non-synthetic) error records
  • Requires both completedFailures >= 5 AND recoverEmitted before escalating
  • Uses a shared autoResumeSpent budget so that once any signature triggers a block, subsequent signatures immediately stop

This matches the PR objective: "attempt 6 → block, attempt 7 → stop."


466-480: Reminder text differentiated by signature type.

Using the key prefix (input: vs target:) to select appropriate reminder text provides clearer guidance to the model based on whether it's repeating exact inputs or probing the same target.


501-503: File tool target detection now works correctly.

The addition of filePath and filepath to target extraction addresses the previous concern about file tools (read, edit, write, lsp) falling back to same_input detection instead of accumulating same_target. Both variants correctly normalize to kind "path".

@Astro-Han Astro-Han force-pushed the claude/issue-229-loop-detection-v1 branch from 60a60a8 to 711e7be Compare April 27, 2026 08:39

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/opencode/src/session/diagnostics.ts`:
- Around line 104-122: truncateForRenderer currently calls JSON.stringify(value)
which can throw for circular structures or unsupported types (e.g. BigInt); wrap
the JSON serialization in a try/catch and fall back to a safe textual
representation (e.g. String(value) or a simple inspected form) when
JSON.stringify throws or returns undefined, then proceed with the existing
Buffer/trim logic using that safe string; update the logic inside the
truncateForRenderer function so it never lets JSON.stringify exceptions
propagate and always produces a string to feed Buffer.from.
- Around line 172-173: targetRepeatCount currently counts any prior record with
the same parentID and targetHash across all tools; change the filter to be
tool-scoped by also comparing the tool identifier so only same-tool +
same-target records are counted (e.g., include something like record.tool ===
input.tool or record.toolID === currentTool), updating the filter used to
compute targetRepeatCount (which currently references input.records,
record.parentID, record.targetHash, and input.parentID) so cross-tool retries do
not affect newTarget determination.

In `@packages/opencode/src/session/loop-renderer.ts`:
- Around line 11-12: The current construction of truncatedError uses the raw
firstErrorLine(state.lastError) which may contain sensitive URLs/paths/headers;
update the flow in loop-renderer.ts to redact/sanitize the result of
firstErrorLine before passing it to SessionDiagnostics.truncateForRenderer (or,
if sanitization cannot be confidently performed, replace with a generic message
like "tool error" for the stop summary). Concretely, add a sanitize/redact step
that strips or masks URLs, file paths, query strings and headers from
firstErrorLine output (or return a safe fallback) and use that sanitized string
when creating truncatedError and when populating the stop summary so no raw
error text is rendered or exported.

In `@packages/opencode/src/session/prompt.ts`:
- Around line 531-538: The early-failure branches in the Effect.gen generator
around applyLoopGate should yield the error instances directly instead of
wrapping them in Effect.fail; specifically, replace the two returns that
currently do return yield* Effect.fail(new BlockedLoopError(...)) and return
yield* Effect.fail(new LoopStopError(...)) with direct yielded error instances
(use the BlockedLoopError and LoopStopError objects as the yielded values inside
the Effect.gen flow where applyLoopGate is handled). Apply the same change to
the analogous branches at lines 586–587 so both places yield the error instances
directly rather than wrapping them with Effect.fail.

In `@packages/opencode/test/session/export.test.ts`:
- Around line 248-289: The test and implementation only treat synthetic loop
parts with action "block" as terminal; update Export.deriveSnapshotDiagnostics
to also consider parts with loopAction === "stop" as terminal (so the synthetic
stop emitted on final escalation is exported), ensuring it picks the latest
timestamped synthetic part whether its action is "block" or "stop"; then adjust
the tests in export.test.ts (those referencing Export.deriveSnapshotDiagnostics
and the expectations on result.loop.last.action/completedFailures) to assert
"stop" where the final synthetic event is a stop (and to still prefer latest
timestamp across child trees as before).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 19cad8f6-a746-455e-88ee-0936344ae683

📥 Commits

Reviewing files that changed from the base of the PR and between 60a60a8 and 711e7be.

📒 Files selected for processing (11)
  • packages/opencode/src/session/diagnostics.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/src/session/processor.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/test/session/loop-gate.test.ts
  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (12)
  • GitHub Check: unit-windows-opencode-config-project
  • GitHub Check: unit-windows-desktop
  • GitHub Check: unit-windows-opencode-session
  • GitHub Check: unit-windows-opencode-server-tools
  • GitHub Check: unit-windows-app
  • GitHub Check: unit-app
  • GitHub Check: unit-opencode
  • GitHub Check: unit-desktop
  • GitHub Check: typecheck
  • GitHub Check: smoke-macos-arm64
  • GitHub Check: e2e-artifacts
  • GitHub Check: analyze-js-ts
🧰 Additional context used
📓 Path-based instructions (2)
packages/opencode/**/*.ts

📄 CodeRabbit inference engine (packages/opencode/AGENTS.md)

packages/opencode/**/*.ts: Use Effect.gen(function* () { ... }) for Effect composition
Use Effect.fn("Domain.method") for named/traced effects and Effect.fnUntraced for internal helpers; these accept pipeable operators as extra arguments to avoid unnecessary outer .pipe() wrappers
Use Effect.callback for callback-based APIs
Prefer DateTime.nowAsDate over new Date(yield* Clock.currentTimeMillis) when you need a Date in Effect code
Use Schema.Class for multi-field data in Effect schemas
Use branded schemas (Schema.brand) for single-value types in Effect
Use Schema.TaggedErrorClass for typed errors in Effect schemas
Use Schema.Defect instead of unknown for defect-like causes in Effect code
In Effect.gen / Effect.fn, prefer yield* new MyError(...) over yield* Effect.fail(new MyError(...)) for direct early-failure branches
Use makeRuntime from src/effect/run-service.ts for all services; it returns { runPromise, runFork, runCallback } backed by a shared memoMap that deduplicates layers
Use InstanceState from src/effect/instance-state.ts for per-directory or per-project state that needs per-instance cleanup; do work directly in the InstanceState.make closure where ScopedCache handles run-once semantics
Use Effect.addFinalizer or Effect.acquireRelease inside the InstanceState.make closure for cleanup (subscriptions, process teardown, etc.)
Use Effect.forkScoped inside the InstanceState.make closure for background stream consumers — the fiber is interrupted when the instance is disposed
Prefer FileSystem.FileSystem instead of raw fs/promises for effectful file I/O in Effect services
Prefer ChildProcessSpawner.ChildProcessSpawner with ChildProcess.make(...) instead of custom process wrappers in Effect services
Prefer HttpClient.HttpClient instead of raw fetch in Effect services
Prefer Path.Path, Config, Clock, and DateTime services when those concerns are already inside Effect code
For backgroun...

Files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/test/session/loop-gate.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/src/session/processor.ts
  • packages/opencode/src/session/diagnostics.ts
packages/opencode/test/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (packages/opencode/test/AGENTS.md)

packages/opencode/test/**/*.test.{ts,tsx}: Use the tmpdir function from fixture/fixture.ts to create temporary directories for tests with automatic cleanup. Use await using syntax to ensure automatic cleanup when the variable goes out of scope.
When using the tmpdir function with git repository support, pass the git: true option to initialize a git repo with a root commit.
Use the config option in tmpdir to write an opencode.json config file during test setup by passing a partial Config.Info object.
Use the init option in tmpdir to define custom setup functions that can return extra data accessible via tmp.extra, and use the dispose option for custom cleanup logic.
Use testEffect(...) from test/lib/effect.ts for tests that exercise Effect services or Effect-based workflows.
Use it.effect(...) when the test should run with TestClock and TestConsole. Use it.live(...) when the test depends on real time, filesystem mtimes, child processes, git, locks, or other live OS behavior.
Prefer Effect-aware helpers from fixture/fixture.ts over building manual runtimes in tests: use tmpdirScoped() for scoped temp directories, provideInstance(dir)(effect) for low-level binding without directory creation, provideTmpdirInstance(...) for single temp instance binding, or provideTmpdirServer(...) for tests that also need the test LLM server.
Define const it = testEffect(...) near the top of the test file and keep the test body inside Effect.gen(function* () { ... }). Yield services directly with yield* MyService.Service or yield* MyTool.
Avoid custom ManagedRuntime, attach(...), or ad hoc run(...) wrappers in Effect tests when testEffect(...) already provides the runtime.
When a test needs instance-local state, prefer provideTmpdirInstance(...) or provideInstance(...) over manual Instance.provide(...) inside Promise-style tests.

Files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/test/session/loop-gate.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
🧠 Learnings (35)
📓 Common learnings
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 264
File: packages/opencode/src/session/diagnostics.ts:255-279
Timestamp: 2026-04-27T08:27:40.791Z
Learning: In `packages/opencode/src/session/diagnostics.ts` (PR `#264`, issue `#229`), target-level loop detection is intentionally tool-scoped. The `real` filter includes `r.tool === input.tool` and target signature keys are formatted as `target:${tool}:${targetHash}`. Cross-tool retries on the same URL/path/query are considered legitimate exploration (e.g., webfetch failed → switch to a different tool), NOT a stuck loop. Only same-tool + same-target accumulation counts toward block/stop. Do NOT suggest removing the tool scope from target signature keys or the `real` filter.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 206
File: packages/app/e2e/prompt/prompt-footer-focus.spec.ts:131-143
Timestamp: 2026-04-24T03:51:54.050Z
Learning: In Astro-Han/pawwork E2E tests (packages/app/e2e/fixtures.ts), `project.prompt(text)` internally calls `trackSession(next.sessionID, active.directory)` after the prompt submission is observed and the active session is resolved. Tests that obtain a `sessionID` from `project.prompt()` do NOT need to call `project.trackSession(sessionID)` manually — it is already registered for teardown. Calling it again would be duplicate ownership.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 247
File: packages/ui/src/components/message-part.tsx:1322-1324
Timestamp: 2026-04-26T16:34:54.895Z
Learning: In Astro-Han/pawwork (`packages/ui/src/components/message-part.tsx`), the `taskId` createMemo and `childSessionId` createMemo both intentionally read only from `partMetadata().sessionId` (populated post-execution), not from `input.task_id` / `input.subagent_session_id`. This has always been the case — the original code never read the input field either. Adding an `input.subagent_session_id` fallback would be a new capability, not a bug fix. Do NOT flag the absence of this fallback as a regression in PR `#247` or future PRs unless there is a concrete case where metadata is not populated.
📚 Learning: 2026-04-20T14:36:04.113Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/app/e2e/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:04.113Z
Learning: Applies to packages/app/e2e/**/*.spec.ts : Use fixture-managed cleanup with `withSession(sdk, title, callback)` for temporary sessions instead of calling `sdk.session.delete(...)` directly

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
📚 Learning: 2026-04-24T03:51:54.050Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 206
File: packages/app/e2e/prompt/prompt-footer-focus.spec.ts:131-143
Timestamp: 2026-04-24T03:51:54.050Z
Learning: In Astro-Han/pawwork E2E tests (packages/app/e2e/fixtures.ts), `project.prompt(text)` internally calls `trackSession(next.sessionID, active.directory)` after the prompt submission is observed and the active session is resolved. Tests that obtain a `sessionID` from `project.prompt()` do NOT need to call `project.trackSession(sessionID)` manually — it is already registered for teardown. Calling it again would be duplicate ownership.

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/test/session/loop-gate.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
📚 Learning: 2026-04-20T14:36:04.113Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/app/e2e/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:04.113Z
Learning: Applies to packages/app/e2e/**/*.spec.ts : Call `project.trackSession(sessionID, directory?)` and `project.trackDirectory(directory)` for any resources created outside the fixture so teardown can clean them up

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Prefer Effect-aware helpers from `fixture/fixture.ts` over building manual runtimes in tests: use `tmpdirScoped()` for scoped temp directories, `provideInstance(dir)(effect)` for low-level binding without directory creation, `provideTmpdirInstance(...)` for single temp instance binding, or `provideTmpdirServer(...)` for tests that also need the test LLM server.

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
📚 Learning: 2026-04-27T08:27:40.791Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 264
File: packages/opencode/src/session/diagnostics.ts:255-279
Timestamp: 2026-04-27T08:27:40.791Z
Learning: In `packages/opencode/src/session/diagnostics.ts` (PR `#264`, issue `#229`), target-level loop detection is intentionally tool-scoped. The `real` filter includes `r.tool === input.tool` and target signature keys are formatted as `target:${tool}:${targetHash}`. Cross-tool retries on the same URL/path/query are considered legitimate exploration (e.g., webfetch failed → switch to a different tool), NOT a stuck loop. Only same-tool + same-target accumulation counts toward block/stop. Do NOT suggest removing the tool scope from target signature keys or the `real` filter.

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/test/session/loop-gate.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/src/session/processor.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Define `const it = testEffect(...)` near the top of the test file and keep the test body inside `Effect.gen(function* () { ... })`. Yield services directly with `yield* MyService.Service` or `yield* MyTool`.

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/test/session/loop-gate.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/src/session/processor.ts
📚 Learning: 2026-04-23T08:51:00.819Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 186
File: packages/opencode/test/plugin/workspace-adaptor.test.ts:139-144
Timestamp: 2026-04-23T08:51:00.819Z
Learning: For pawwork tests under packages/opencode/test/**, auth.json teardown may intentionally combine `Filesystem.write` (from `packages/opencode/src/util/filesystem.ts`) with `node:fs/promises` `unlink` for cleanup. Do not flag this as inconsistent style; it is the established/intentional pattern because `Filesystem` does not provide a `remove`/`unlink` helper.

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/test/session/loop-gate.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
📚 Learning: 2026-04-26T16:34:54.895Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 247
File: packages/ui/src/components/message-part.tsx:1322-1324
Timestamp: 2026-04-26T16:34:54.895Z
Learning: In Astro-Han/pawwork (`packages/ui/src/components/message-part.tsx`), the `taskId` createMemo and `childSessionId` createMemo both intentionally read only from `partMetadata().sessionId` (populated post-execution), not from `input.task_id` / `input.subagent_session_id`. This has always been the case — the original code never read the input field either. Adding an `input.subagent_session_id` fallback would be a new capability, not a bug fix. Do NOT flag the absence of this fallback as a regression in PR `#247` or future PRs unless there is a concrete case where metadata is not populated.

Applied to files:

  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-24T05:39:56.086Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 208
File: packages/app/src/components/prompt-input.tsx:1569-1611
Timestamp: 2026-04-24T05:39:56.086Z
Learning: In Astro-Han/pawwork `packages/app/src/components/prompt-input.tsx`, after the composer unification in PR `#208` (fixed in commit 5d810aa):
- `SendButton.disabled` does NOT gate on `store.mode !== "normal"`. Shell mode has a fully visible, clickable orange submit button that calls `handleSubmit` directly (same path as the Enter key in `handleKeyDown`). Do NOT suggest re-adding the mode gate.
- `SendButton` does NOT use the `buttons()` spring opacity animation (`style={buttons()}`). It is always fully visible regardless of mode.
- `WorkspaceChip` is gated on `props.homeMode && store.mode === "normal"` so it hides in shell mode (preventing it from appearing isolated/bright while neighboring controls fade).
- The left-side chip group (`aria-hidden={store.mode !== "normal"}`) covers attach/model/variant/workspace controls only; `SendButton` remains in a separate right-side sibling div.

Applied to files:

  • packages/opencode/src/session/loop-renderer.ts
📚 Learning: 2026-04-24T17:08:44.294Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 224
File: packages/app/src/i18n/zh.ts:0-0
Timestamp: 2026-04-24T17:08:44.294Z
Learning: In Astro-Han/pawwork PR `#224`, the first-occurrence `PawWork 爪印` branding rule originally specified in issue `#196` was superseded by an updated Chinese-branding spec. On all zh UI surfaces in `packages/app/src/i18n/zh.ts` (e.g., `dialog.model.unpaid.freeModels.title`, `session.new.subtitle`, `sidebar.gettingStarted.line1`), the correct and intentional target is fully localized `爪印` branding — no `PawWork` prefix. Do NOT flag these strings as missing the first-occurrence `PawWork 爪印` rule in future reviews.

Applied to files:

  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-22T09:32:58.310Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 126
File: packages/ui/src/theme/context.tsx:11-16
Timestamp: 2026-04-22T09:32:58.310Z
Learning: In Astro-Han/pawwork (`packages/ui/src/theme/context.tsx` and related files), the renaming of localStorage theme keys from `opencode-*` to `pawwork-*` (THEME_ID, COLOR_SCHEME, THEME_CSS_LIGHT, THEME_CSS_DARK) is intentional and should NOT include a migration path from the old keys. Migrating would re-couple PawWork and OpenCode browser storage namespaces, which the PR is explicitly designed to avoid. A reset to the PawWork default theme on upgrade is acceptable by design.

Applied to files:

  • packages/opencode/src/session/loop-renderer.ts
📚 Learning: 2026-04-25T11:52:40.349Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 233
File: packages/desktop-electron/src/main/server.ts:60-60
Timestamp: 2026-04-25T11:52:40.349Z
Learning: In Astro-Han/pawwork (`packages/desktop-electron/src/main/index.ts`), `ensureLoopbackNoProxy()` (defined at line 551) is called inside `setupApp()` at line 263, which runs synchronously before `initialize()` (line 321) and `spawnLocalServer()` (line 395). It unconditionally upserts `127.0.0.1`, `localhost`, and `::1` into both `process.env.NO_PROXY` and `process.env.no_proxy`. This guarantees that `configureProxyDispatcher(process.env)` in `server.ts` always receives a `process.env` that already excludes loopback from proxying. Do not flag the absence of a loopback-merge inside `configureProxyDispatcher` or `normalizeProxyConfig` — the exclusion is already enforced at a higher layer before those functions are ever called.

Applied to files:

  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-23T15:26:07.250Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 193
File: packages/app/src/pages/layout/sidebar-items.tsx:102-107
Timestamp: 2026-04-23T15:26:07.250Z
Learning: In Astro-Han/pawwork (`packages/app/src/pages/layout/sidebar-items.tsx`), the `indicator()` function in `SessionRow` intentionally renders `props.leadingSlot` (the pin button) only as a fallback when no status indicator (running/permission/error/unseen) is active. When a higher-priority status wins the slot, the pin button is removed from the DOM — this is a deliberate design choice for the merged leading slot (`#150`). The keyboard unpin path is preserved via: (1) focusing the row anchor triggers `group-focus-within` which reveals the dots menu trigger, then Tab → Enter → "Unpin Session"; (2) the context menu (right-click / Shift+F10) exposes "Unpin Session". The "always render + CSS overlay" approach was considered but rejected due to z-index/pointer-events complexity; residual `...` slot behavior is tracked in `#192`. Do NOT flag the absence of the pin button from the DOM when a status is active as an accessibility regression.

Applied to files:

  • packages/opencode/src/session/loop-renderer.ts
📚 Learning: 2026-04-26T15:35:31.757Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 245
File: packages/opencode/src/question/index.ts:21-24
Timestamp: 2026-04-26T15:35:31.757Z
Learning: In Astro-Han/pawwork (`packages/opencode/src/question/index.ts`), `Question.Option.description` is intentionally **required** (`z.string()`, not `.optional()`). This matches the upstream opencode contract and all current callers (e.g. `PlanExitTool`) always supply a description. The defensive `<Show when={props.description}>` rendering in `session-question-dock.tsx` is a standard guard, not a signal that the field is intended to be optional. Do NOT suggest making `Option.description` optional without a dedicated follow-up that covers schema + tool description + dock copy + tests.

Applied to files:

  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/src/session/processor.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-20T14:21:56.373Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 71
File: packages/app/src/components/session/session-status-connections.tsx:146-147
Timestamp: 2026-04-20T14:21:56.373Z
Learning: In the Astro-Han/pawwork repository (SolidJS app), `sync.data.config` is always initialized to `{}` at `packages/app/src/context/global-sync.tsx` line 71 and is never `undefined` at runtime. Non-optional property access like `sync.data.config.plugin` is intentional and consistent with the pattern used in `packages/app/src/components/status-popover-body.tsx` line 243. Do not flag `sync.data.config.plugin` as needing optional chaining.

Applied to files:

  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/src/session/export.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Use the `tmpdir` function from `fixture/fixture.ts` to create temporary directories for tests with automatic cleanup. Use `await using` syntax to ensure automatic cleanup when the variable goes out of scope.

Applied to files:

  • packages/opencode/test/session/export.test.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : When a test needs instance-local state, prefer `provideTmpdirInstance(...)` or `provideInstance(...)` over manual `Instance.provide(...)` inside Promise-style tests.

Applied to files:

  • packages/opencode/test/session/export.test.ts
📚 Learning: 2026-04-20T14:36:04.113Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/app/e2e/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:04.113Z
Learning: Applies to packages/app/e2e/packages/app/src/testing/**/*.ts : Test-only hooks must be inert unless explicitly enabled and should not add normal-runtime listeners, reactive subscriptions, or per-update allocations

Applied to files:

  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
📚 Learning: 2026-04-22T09:32:54.556Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 126
File: packages/opencode/test/provider/provider.test.ts:64-85
Timestamp: 2026-04-22T09:32:54.556Z
Learning: In `packages/opencode/test/provider/provider.test.ts`, the file intentionally uses AppRuntime-based async helpers (`run`, `list`, `getProvider`, etc.) rather than `testEffect(...)` for all tests. Converting individual tests to `testEffect` while leaving the rest on the async pattern would create internal inconsistency. A full harness migration of this file is the right approach if the pattern needs to change, but that should be a separate PR.

Applied to files:

  • packages/opencode/test/session/loop-renderer.test.ts
📚 Learning: 2026-04-24T00:02:53.315Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 203
File: packages/app/e2e/sidebar/sidebar-session-links.spec.ts:34-55
Timestamp: 2026-04-24T00:02:53.315Z
Learning: In Astro-Han/pawwork E2E tests (`packages/app/e2e/**/*.spec.ts`), `project.trackDirectory()` and `project.trackSession()` cannot be called before `project.open()` — the `project` fixture throws until `open()` initializes its internal state. The correct pattern is: call `project.trackSession(sessionID)` from inside the `beforeGoto` callback (where state already exists), call `project.trackDirectory(directory)` and cross-workspace `project.trackSession(id, directory)` immediately after `project.open()` returns, and rely on explicit `finally` cleanup (e.g. `cleanupSession` / `cleanupTestProject`) for any resources created before `open()` that cannot yet be tracked via the fixture.

Applied to files:

  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/loop-gate.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-25T12:52:36.999Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 234
File: packages/opencode/src/session/export.ts:75-88
Timestamp: 2026-04-25T12:52:36.999Z
Learning: In `packages/opencode/src/session/export.ts` (Astro-Han/pawwork), the `extractReasonFromCause` function intentionally uses a lightweight cast-based inspection of the Cause `reasons` array instead of `Cause.failures()` / `Cause.defects()` utilities. This is a deliberate choice: the function is diagnostic-only (used to populate a best-effort reason string for export diagnostics), and the maintainer explicitly prefers not to add coupling to Effect's Cause API surface for this purpose. Do not flag this pattern as fragile or suggest replacing it with Cause utilities.

Applied to files:

  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-25T12:52:32.462Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 234
File: packages/desktop-electron/src/main/ipc.ts:238-263
Timestamp: 2026-04-25T12:52:32.462Z
Learning: In Astro-Han/pawwork (`packages/desktop-electron/src/main/ipc.ts`), `deps.getServerReadyData()` (backed by `serverReady.promise` in `index.ts`) resolves once at server startup and remains settled; it is not expected to reject in practice. Do not flag the absence of a try-catch around it in the `export-session` IPC handler — the network/fetch layer in `server-client.ts` already has a 10-second AbortController timeout and returns a typed `{ok: false, error}` payload, covering the real failure modes.

Applied to files:

  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/src/session/processor.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-24T13:03:10.835Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 222
File: packages/desktop-electron/src/main/index.ts:686-692
Timestamp: 2026-04-24T13:03:10.835Z
Learning: In `packages/desktop-electron/src/main/index.ts`, the `checkForUpdates()` function intentionally uses recursive self-invocation for the "Retry" path in the update-check failure dialog. This is mandated by the v5.2 design spec (`#213`): "Await the retry recursion and log any rejection so support can see repeated failures." Because retries are user-paced (require a button click), all prior async frames have already unwound through microtasks before the next attempt, so there is no stack/frame-nesting problem in practice. Do not suggest refactoring this to an iterative loop.

Applied to files:

  • packages/opencode/src/session/export.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-22T08:49:47.800Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 126
File: packages/desktop-electron/src/main/index-sidecar-source.test.ts:3-11
Timestamp: 2026-04-22T08:49:47.800Z
Learning: In `packages/desktop-electron/src/main/index-sidecar-source.test.ts` (Astro-Han/pawwork), the test intentionally uses `expect(source).toContain` / `expect(source).not.toContain` string matching against the raw `index.ts` source text as a lightweight sidecar contract guard. The maintainer has explicitly chosen not to introduce an AST parser (e.g., `babel/parser` or acorn) for this purpose. Do not flag these string-based assertions as fragile or suggest converting them to AST-based matching.

Applied to files:

  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
📚 Learning: 2026-04-25T12:52:47.074Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 234
File: packages/opencode/src/share/session.ts:27-27
Timestamp: 2026-04-25T12:52:47.074Z
Learning: In `packages/opencode/src/share/session.ts`, the local `ensureEnabled` closure is intentionally duplicated from `ShareRuntime.ensureEnabled` in `runtime.ts`. Using the shared `ShareRuntime.ensureEnabled` effect directly would leak `CloudShareGate` back into the `R` (requirement) type of `share`, `unshare`, and `create`, because the shared effect re-yields the service rather than capturing an already-resolved gate reference. The current pattern resolves `gate` once at the top of the outer `Effect.gen` and then closes over it in a local `ensureEnabled`, keeping the inner effects' requirement types clean. A comment in the file points to `runtime.ts` to make the intentional duplication discoverable. The real fix would be refactoring `ensureEnabled` to accept a gate parameter, but that change is larger than the DRY benefit warrants. Do not flag this duplication as a maintenance issue.

Applied to files:

  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/loop-gate.test.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-23T15:25:31.118Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 193
File: packages/app/e2e/sidebar/sidebar-leading-slot.spec.ts:5-55
Timestamp: 2026-04-23T15:25:31.118Z
Learning: In Astro-Han/pawwork E2E tests, reaching a real "running" session state requires the `project` fixture (for model bootstrap) plus `llm.wait(1)` orchestration. The bare `sdk` fixture used by `withSession` does not trigger an LLM call even with `agent: "build"` + `system` prompt set via `sdk.session.promptAsync`, so simulating running state is not lightweight in the current test infrastructure.

Applied to files:

  • packages/opencode/src/session/prompt.ts
  • packages/opencode/src/session/processor.ts
📚 Learning: 2026-04-23T08:51:04.230Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 186
File: packages/opencode/test/plugin/workspace-adaptor.test.ts:139-144
Timestamp: 2026-04-23T08:51:04.230Z
Learning: In Astro-Han/pawwork (`packages/opencode/src/util/filesystem.ts`), the `Filesystem` utility does NOT expose a `remove` or `unlink` helper. The established repository pattern for auth.json teardown in tests (e.g. `provider.test.ts`, `amazon-bedrock.test.ts`, `workspace-adaptor.test.ts`) is to combine `Filesystem.write` with `node:fs/promises unlink`. Do not flag this mixed usage as inconsistent — it is the correct and intentional pattern.

Applied to files:

  • packages/opencode/src/session/prompt.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : In `Effect.gen` / `Effect.fn`, prefer `yield* new MyError(...)` over `yield* Effect.fail(new MyError(...))` for direct early-failure branches

Applied to files:

  • packages/opencode/src/session/prompt.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : For background loops or scheduled tasks, use `Effect.repeat` or `Effect.schedule` with `Effect.forkScoped` in the layer definition

Applied to files:

  • packages/opencode/src/session/prompt.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Use `testEffect(...)` from `test/lib/effect.ts` for tests that exercise Effect services or Effect-based workflows.

Applied to files:

  • packages/opencode/test/session/prompt-effect.test.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Use `it.effect(...)` when the test should run with `TestClock` and `TestConsole`. Use `it.live(...)` when the test depends on real time, filesystem mtimes, child processes, git, locks, or other live OS behavior.

Applied to files:

  • packages/opencode/test/session/prompt-effect.test.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Avoid custom `ManagedRuntime`, `attach(...)`, or ad hoc `run(...)` wrappers in Effect tests when `testEffect(...)` already provides the runtime.

Applied to files:

  • packages/opencode/test/session/prompt-effect.test.ts
📚 Learning: 2026-04-24T06:50:04.597Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 211
File: packages/opencode/src/provider/models.ts:291-304
Timestamp: 2026-04-24T06:50:04.597Z
Learning: In `packages/opencode/src/provider/models.ts`, the `getWithVersion()` function intentionally uses an unbounded `while (true)` retry loop. It only retries when `catalogVersion` changes between the pre- and post-`Data()` read, which is tied to refresh races. The maintainer (Astro-Han) explicitly rejected adding a MAX_RETRIES cap because a hard failure on provider state bootstrap (due to an arbitrary retry limit) is a worse failure mode than retrying until the catalog stabilizes. The loop is expected to self-settle once concurrent writes stop.

Applied to files:

  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-24T06:50:02.712Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 211
File: packages/opencode/src/provider/models.ts:113-179
Timestamp: 2026-04-24T06:50:02.712Z
Learning: In Astro-Han/pawwork (`packages/opencode/src/provider/models.ts`), `PublishModel` and `PublishProvider` intentionally duplicate fields from `Model` and `Provider` with looser optionality. The split is a deliberate behavior boundary: `PublishModel`/`PublishProvider` (with `.passthrough()`) are used for lenient publish-time validation of incoming catalogs, while `Model`/`Provider` are used for strict runtime normalization. Do not flag this duplication as a maintenance burden or suggest extracting a shared base schema — the explicit separation is intentional and tied to the models-refresh reliability path introduced in PR `#211`.

Applied to files:

  • packages/opencode/src/session/diagnostics.ts
🪛 Betterleaks (1.1.2)
packages/opencode/test/session/diagnostics.test.ts

[high] 253-253: Discovered a potential authorization token provided in a curl command header, which could compromise the curl accessed resource.

(curl-auth-header)

Comment thread packages/opencode/src/session/diagnostics.ts
Comment thread packages/opencode/src/session/diagnostics.ts Outdated
Comment thread packages/opencode/src/session/loop-renderer.ts Outdated
Comment thread packages/opencode/src/session/prompt.ts
Comment thread packages/opencode/test/session/export.test.ts
@Astro-Han Astro-Han force-pushed the claude/issue-229-loop-detection-v1 branch from 711e7be to 0e106cf Compare April 27, 2026 08:57
@Astro-Han

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Apr 27, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/opencode/src/session/prompt.ts (1)

471-477: ⚠️ Potential issue | 🟠 Major

Gate StructuredOutput after a synthetic stop.

These wrappers only protect registry/MCP tools. In json_schema turns, StructuredOutput is added later without the same processor.hasStopped(parentID) short-circuit, so an auto-resume after recordSyntheticStop(...) can still execute that tool and effectively reopen a turn that was supposed to end. Please route StructuredOutput through the same stop guard, or explicitly fail it once the parent turn has been stopped.

Also applies to: 531-538, 580-587

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/opencode/src/session/prompt.ts` around lines 471 - 477, The
StructuredOutput tool is not being gated by the processor.hasStopped(parentID)
short-circuit like other registry/MCP tools, so after recordSyntheticStop(...)
an auto-resume can still invoke StructuredOutput and reopen a turn; update
resolveTools (and the analogous blocks around lines noted, e.g., the json_schema
handling) to route StructuredOutput through the same stop guard by checking
processor.hasStopped(parentID) before allowing execution or explicitly
fail/throw when the parent turn is stopped (ensure the same short-circuit path
used for other tools is applied to StructuredOutput).
♻️ Duplicate comments (1)
packages/opencode/src/session/loop-renderer.ts (1)

11-13: ⚠️ Potential issue | 🟠 Major

Scrub non-URL secrets before echoing lastError into the stop message.

truncatedError only removes embedded URLs. Errors like open /Users/alice/private.txt: permission denied or Authorization: Bearer secret still get copied verbatim into the synthetic assistant text/export in both the target and input stop branches.

🔒 Suggested hardening
-    const scrubbedError = errorLine ? scrubURLsInText(errorLine) : undefined
+    const scrubbedError = errorLine ? scrubErrorText(errorLine) : undefined
     const truncatedError = scrubbedError ? SessionDiagnostics.truncateForRenderer(scrubbedError) : undefined
@@
   function scrubURLsInText(text: string): string {
     return text.replace(/(https?:\/\/[^\s,;)]+)/g, (match) => stripQueryAndFragment(match))
   }
+
+  function scrubErrorText(text: string): string {
+    return scrubURLsInText(text)
+      .replace(/['"`][^'"`]*['"`]/g, "<quoted>")
+      .replace(/[A-Za-z]:\\[^\s]+/g, "<path>")
+      .replace(/(^|\s)\/[^\s,;)]+/g, "$1<path>")
+  }

Also applies to: 37-46

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/opencode/src/session/loop-renderer.ts` around lines 11 - 13, The
stop-message flow currently derives truncatedError from
firstErrorLine(state.lastError) -> scrubURLsInText(...) ->
SessionDiagnostics.truncateForRenderer(...), which only removes URLs; update the
pipeline so non-URL secrets are scrubbed as well before truncation and before
embedding into synthetic assistant text: add or call a scrubSecretsInText (or
reuse existing sanitizer) to sanitize the output of
firstErrorLine(state.lastError) (in the same places where truncatedError is
computed and in the similar block at lines 37-46), then pass the scrubbed result
into SessionDiagnostics.truncateForRenderer; ensure you reference and replace
the current scrubURLsInText usage for the variables errorLine, scrubbedError,
and truncatedError so no raw file paths, tokens, or authorization headers make
it into the stop message.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/opencode/src/session/diagnostics.ts`:
- Around line 486-489: The message pushed when sawTarget is true incorrectly
describes "same class of tool error" whereas the branch actually fires for
repeated failures against the same target; update the string in the lines.push
call (the branch guarded by the sawTarget variable in diagnostics.ts) to
explicitly say the model has repeatedly failed against the same target (even if
errors differ) and advise not to keep retrying but to change approach, identify
the failing target, or summarize the blocker; keep the rest of the surrounding
logic intact.
- Around line 296-308: The reminders created in the loop (inside the for-of over
candidates where you push into newReminders) always set type: "input_repeat"
even for target: signatures; update that push to choose a target-specific type
when sigKey denotes a target signature (e.g., detect target via the sigKey value
or matcher metadata) and set type to something like "target_input_repeat" (or
your agreed target-specific string) instead of the generic "input_repeat" so
persisted diagnostics correctly reflect target vs non-target reminders; change
the type assignment in the newReminders.push call (referencing sigKey, matcher,
real, newReminders) accordingly.

In `@packages/opencode/test/session/diagnostics.test.ts`:
- Around line 252-254: Replace the secret-shaped token in the test fixture used
to build the command summary: inside the SessionDiagnostics.targetSummary call
(the test that assigns to `command`), change the string "Authorization: Bearer
short-token" to a clearly fake placeholder such as "Authorization: Bearer
TEST_TOKEN" or "Authorization: Bearer <token>" so the test behavior is unchanged
but it no longer trips secret scanning.

---

Outside diff comments:
In `@packages/opencode/src/session/prompt.ts`:
- Around line 471-477: The StructuredOutput tool is not being gated by the
processor.hasStopped(parentID) short-circuit like other registry/MCP tools, so
after recordSyntheticStop(...) an auto-resume can still invoke StructuredOutput
and reopen a turn; update resolveTools (and the analogous blocks around lines
noted, e.g., the json_schema handling) to route StructuredOutput through the
same stop guard by checking processor.hasStopped(parentID) before allowing
execution or explicitly fail/throw when the parent turn is stopped (ensure the
same short-circuit path used for other tools is applied to StructuredOutput).

---

Duplicate comments:
In `@packages/opencode/src/session/loop-renderer.ts`:
- Around line 11-13: The stop-message flow currently derives truncatedError from
firstErrorLine(state.lastError) -> scrubURLsInText(...) ->
SessionDiagnostics.truncateForRenderer(...), which only removes URLs; update the
pipeline so non-URL secrets are scrubbed as well before truncation and before
embedding into synthetic assistant text: add or call a scrubSecretsInText (or
reuse existing sanitizer) to sanitize the output of
firstErrorLine(state.lastError) (in the same places where truncatedError is
computed and in the similar block at lines 37-46), then pass the scrubbed result
into SessionDiagnostics.truncateForRenderer; ensure you reference and replace
the current scrubURLsInText usage for the variables errorLine, scrubbedError,
and truncatedError so no raw file paths, tokens, or authorization headers make
it into the stop message.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 43e5177c-723c-43ef-992e-0477eaf757aa

📥 Commits

Reviewing files that changed from the base of the PR and between 711e7be and 0e106cf.

📒 Files selected for processing (11)
  • packages/opencode/src/session/diagnostics.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/src/session/processor.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/test/session/loop-gate.test.ts
  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: unit-windows-app
  • GitHub Check: unit-windows-desktop
  • GitHub Check: unit-windows-opencode-config-project
  • GitHub Check: unit-windows-opencode-server-tools
  • GitHub Check: unit-windows-opencode-session
  • GitHub Check: unit-opencode
🧰 Additional context used
📓 Path-based instructions (2)
packages/opencode/**/*.ts

📄 CodeRabbit inference engine (packages/opencode/AGENTS.md)

packages/opencode/**/*.ts: Use Effect.gen(function* () { ... }) for Effect composition
Use Effect.fn("Domain.method") for named/traced effects and Effect.fnUntraced for internal helpers; these accept pipeable operators as extra arguments to avoid unnecessary outer .pipe() wrappers
Use Effect.callback for callback-based APIs
Prefer DateTime.nowAsDate over new Date(yield* Clock.currentTimeMillis) when you need a Date in Effect code
Use Schema.Class for multi-field data in Effect schemas
Use branded schemas (Schema.brand) for single-value types in Effect
Use Schema.TaggedErrorClass for typed errors in Effect schemas
Use Schema.Defect instead of unknown for defect-like causes in Effect code
In Effect.gen / Effect.fn, prefer yield* new MyError(...) over yield* Effect.fail(new MyError(...)) for direct early-failure branches
Use makeRuntime from src/effect/run-service.ts for all services; it returns { runPromise, runFork, runCallback } backed by a shared memoMap that deduplicates layers
Use InstanceState from src/effect/instance-state.ts for per-directory or per-project state that needs per-instance cleanup; do work directly in the InstanceState.make closure where ScopedCache handles run-once semantics
Use Effect.addFinalizer or Effect.acquireRelease inside the InstanceState.make closure for cleanup (subscriptions, process teardown, etc.)
Use Effect.forkScoped inside the InstanceState.make closure for background stream consumers — the fiber is interrupted when the instance is disposed
Prefer FileSystem.FileSystem instead of raw fs/promises for effectful file I/O in Effect services
Prefer ChildProcessSpawner.ChildProcessSpawner with ChildProcess.make(...) instead of custom process wrappers in Effect services
Prefer HttpClient.HttpClient instead of raw fetch in Effect services
Prefer Path.Path, Config, Clock, and DateTime services when those concerns are already inside Effect code
For backgroun...

Files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/loop-gate.test.ts
  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/src/session/diagnostics.ts
  • packages/opencode/src/session/processor.ts
packages/opencode/test/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (packages/opencode/test/AGENTS.md)

packages/opencode/test/**/*.test.{ts,tsx}: Use the tmpdir function from fixture/fixture.ts to create temporary directories for tests with automatic cleanup. Use await using syntax to ensure automatic cleanup when the variable goes out of scope.
When using the tmpdir function with git repository support, pass the git: true option to initialize a git repo with a root commit.
Use the config option in tmpdir to write an opencode.json config file during test setup by passing a partial Config.Info object.
Use the init option in tmpdir to define custom setup functions that can return extra data accessible via tmp.extra, and use the dispose option for custom cleanup logic.
Use testEffect(...) from test/lib/effect.ts for tests that exercise Effect services or Effect-based workflows.
Use it.effect(...) when the test should run with TestClock and TestConsole. Use it.live(...) when the test depends on real time, filesystem mtimes, child processes, git, locks, or other live OS behavior.
Prefer Effect-aware helpers from fixture/fixture.ts over building manual runtimes in tests: use tmpdirScoped() for scoped temp directories, provideInstance(dir)(effect) for low-level binding without directory creation, provideTmpdirInstance(...) for single temp instance binding, or provideTmpdirServer(...) for tests that also need the test LLM server.
Define const it = testEffect(...) near the top of the test file and keep the test body inside Effect.gen(function* () { ... }). Yield services directly with yield* MyService.Service or yield* MyTool.
Avoid custom ManagedRuntime, attach(...), or ad hoc run(...) wrappers in Effect tests when testEffect(...) already provides the runtime.
When a test needs instance-local state, prefer provideTmpdirInstance(...) or provideInstance(...) over manual Instance.provide(...) inside Promise-style tests.

Files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/loop-gate.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
🧠 Learnings (41)
📓 Common learnings
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 264
File: packages/opencode/src/session/diagnostics.ts:255-279
Timestamp: 2026-04-27T08:27:40.791Z
Learning: In `packages/opencode/src/session/diagnostics.ts` (PR `#264`, issue `#229`), target-level loop detection is intentionally tool-scoped. The `real` filter includes `r.tool === input.tool` and target signature keys are formatted as `target:${tool}:${targetHash}`. Cross-tool retries on the same URL/path/query are considered legitimate exploration (e.g., webfetch failed → switch to a different tool), NOT a stuck loop. Only same-tool + same-target accumulation counts toward block/stop. Do NOT suggest removing the tool scope from target signature keys or the `real` filter.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 206
File: packages/app/e2e/prompt/prompt-footer-focus.spec.ts:131-143
Timestamp: 2026-04-24T03:51:54.050Z
Learning: In Astro-Han/pawwork E2E tests (packages/app/e2e/fixtures.ts), `project.prompt(text)` internally calls `trackSession(next.sessionID, active.directory)` after the prompt submission is observed and the active session is resolved. Tests that obtain a `sessionID` from `project.prompt()` do NOT need to call `project.trackSession(sessionID)` manually — it is already registered for teardown. Calling it again would be duplicate ownership.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 247
File: packages/ui/src/components/message-part.tsx:1322-1324
Timestamp: 2026-04-26T16:34:54.895Z
Learning: In Astro-Han/pawwork (`packages/ui/src/components/message-part.tsx`), the `taskId` createMemo and `childSessionId` createMemo both intentionally read only from `partMetadata().sessionId` (populated post-execution), not from `input.task_id` / `input.subagent_session_id`. This has always been the case — the original code never read the input field either. Adding an `input.subagent_session_id` fallback would be a new capability, not a bug fix. Do NOT flag the absence of this fallback as a regression in PR `#247` or future PRs unless there is a concrete case where metadata is not populated.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 234
File: packages/opencode/src/share/session.ts:27-27
Timestamp: 2026-04-25T12:52:47.074Z
Learning: In `packages/opencode/src/share/session.ts`, the local `ensureEnabled` closure is intentionally duplicated from `ShareRuntime.ensureEnabled` in `runtime.ts`. Using the shared `ShareRuntime.ensureEnabled` effect directly would leak `CloudShareGate` back into the `R` (requirement) type of `share`, `unshare`, and `create`, because the shared effect re-yields the service rather than capturing an already-resolved gate reference. The current pattern resolves `gate` once at the top of the outer `Effect.gen` and then closes over it in a local `ensureEnabled`, keeping the inner effects' requirement types clean. A comment in the file points to `runtime.ts` to make the intentional duplication discoverable. The real fix would be refactoring `ensureEnabled` to accept a gate parameter, but that change is larger than the DRY benefit warrants. Do not flag this duplication as a maintenance issue.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 193
File: packages/app/e2e/sidebar/sidebar-leading-slot.spec.ts:5-55
Timestamp: 2026-04-23T15:25:31.118Z
Learning: In Astro-Han/pawwork E2E tests, reaching a real "running" session state requires the `project` fixture (for model bootstrap) plus `llm.wait(1)` orchestration. The bare `sdk` fixture used by `withSession` does not trigger an LLM call even with `agent: "build"` + `system` prompt set via `sdk.session.promptAsync`, so simulating running state is not lightweight in the current test infrastructure.
📚 Learning: 2026-04-20T14:36:04.113Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/app/e2e/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:04.113Z
Learning: Applies to packages/app/e2e/**/*.spec.ts : Use fixture-managed cleanup with `withSession(sdk, title, callback)` for temporary sessions instead of calling `sdk.session.delete(...)` directly

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
📚 Learning: 2026-04-27T08:27:40.791Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 264
File: packages/opencode/src/session/diagnostics.ts:255-279
Timestamp: 2026-04-27T08:27:40.791Z
Learning: In `packages/opencode/src/session/diagnostics.ts` (PR `#264`, issue `#229`), target-level loop detection is intentionally tool-scoped. The `real` filter includes `r.tool === input.tool` and target signature keys are formatted as `target:${tool}:${targetHash}`. Cross-tool retries on the same URL/path/query are considered legitimate exploration (e.g., webfetch failed → switch to a different tool), NOT a stuck loop. Only same-tool + same-target accumulation counts toward block/stop. Do NOT suggest removing the tool scope from target signature keys or the `real` filter.

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/loop-gate.test.ts
  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/src/session/diagnostics.ts
  • packages/opencode/src/session/processor.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Prefer Effect-aware helpers from `fixture/fixture.ts` over building manual runtimes in tests: use `tmpdirScoped()` for scoped temp directories, `provideInstance(dir)(effect)` for low-level binding without directory creation, `provideTmpdirInstance(...)` for single temp instance binding, or `provideTmpdirServer(...)` for tests that also need the test LLM server.

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Define `const it = testEffect(...)` near the top of the test file and keep the test body inside `Effect.gen(function* () { ... })`. Yield services directly with `yield* MyService.Service` or `yield* MyTool`.

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/loop-gate.test.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/src/session/processor.ts
📚 Learning: 2026-04-24T03:51:54.050Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 206
File: packages/app/e2e/prompt/prompt-footer-focus.spec.ts:131-143
Timestamp: 2026-04-24T03:51:54.050Z
Learning: In Astro-Han/pawwork E2E tests (packages/app/e2e/fixtures.ts), `project.prompt(text)` internally calls `trackSession(next.sessionID, active.directory)` after the prompt submission is observed and the active session is resolved. Tests that obtain a `sessionID` from `project.prompt()` do NOT need to call `project.trackSession(sessionID)` manually — it is already registered for teardown. Calling it again would be duplicate ownership.

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/loop-gate.test.ts
  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-20T14:36:04.113Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/app/e2e/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:04.113Z
Learning: Applies to packages/app/e2e/**/*.spec.ts : Call `project.trackSession(sessionID, directory?)` and `project.trackDirectory(directory)` for any resources created outside the fixture so teardown can clean them up

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Use `it.effect(...)` when the test should run with `TestClock` and `TestConsole`. Use `it.live(...)` when the test depends on real time, filesystem mtimes, child processes, git, locks, or other live OS behavior.

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts
📚 Learning: 2026-04-22T09:32:54.556Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 126
File: packages/opencode/test/provider/provider.test.ts:64-85
Timestamp: 2026-04-22T09:32:54.556Z
Learning: In `packages/opencode/test/provider/provider.test.ts`, the file intentionally uses AppRuntime-based async helpers (`run`, `list`, `getProvider`, etc.) rather than `testEffect(...)` for all tests. Converting individual tests to `testEffect` while leaving the rest on the async pattern would create internal inconsistency. A full harness migration of this file is the right approach if the pattern needs to change, but that should be a separate PR.

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/loop-renderer.test.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Use `testEffect(...)` from `test/lib/effect.ts` for tests that exercise Effect services or Effect-based workflows.

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Avoid custom `ManagedRuntime`, `attach(...)`, or ad hoc `run(...)` wrappers in Effect tests when `testEffect(...)` already provides the runtime.

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/src/session/prompt.ts
📚 Learning: 2026-04-23T08:51:00.819Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 186
File: packages/opencode/test/plugin/workspace-adaptor.test.ts:139-144
Timestamp: 2026-04-23T08:51:00.819Z
Learning: For pawwork tests under packages/opencode/test/**, auth.json teardown may intentionally combine `Filesystem.write` (from `packages/opencode/src/util/filesystem.ts`) with `node:fs/promises` `unlink` for cleanup. Do not flag this as inconsistent style; it is the established/intentional pattern because `Filesystem` does not provide a `remove`/`unlink` helper.

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/loop-gate.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
📚 Learning: 2026-04-25T12:52:47.074Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 234
File: packages/opencode/src/share/session.ts:27-27
Timestamp: 2026-04-25T12:52:47.074Z
Learning: In `packages/opencode/src/share/session.ts`, the local `ensureEnabled` closure is intentionally duplicated from `ShareRuntime.ensureEnabled` in `runtime.ts`. Using the shared `ShareRuntime.ensureEnabled` effect directly would leak `CloudShareGate` back into the `R` (requirement) type of `share`, `unshare`, and `create`, because the shared effect re-yields the service rather than capturing an already-resolved gate reference. The current pattern resolves `gate` once at the top of the outer `Effect.gen` and then closes over it in a local `ensureEnabled`, keeping the inner effects' requirement types clean. A comment in the file points to `runtime.ts` to make the intentional duplication discoverable. The real fix would be refactoring `ensureEnabled` to accept a gate parameter, but that change is larger than the DRY benefit warrants. Do not flag this duplication as a maintenance issue.

Applied to files:

  • packages/opencode/test/session/loop-gate.test.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/prompt-effect.test.ts
📚 Learning: 2026-04-24T00:02:53.315Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 203
File: packages/app/e2e/sidebar/sidebar-session-links.spec.ts:34-55
Timestamp: 2026-04-24T00:02:53.315Z
Learning: In Astro-Han/pawwork E2E tests (`packages/app/e2e/**/*.spec.ts`), `project.trackDirectory()` and `project.trackSession()` cannot be called before `project.open()` — the `project` fixture throws until `open()` initializes its internal state. The correct pattern is: call `project.trackSession(sessionID)` from inside the `beforeGoto` callback (where state already exists), call `project.trackDirectory(directory)` and cross-workspace `project.trackSession(id, directory)` immediately after `project.open()` returns, and rely on explicit `finally` cleanup (e.g. `cleanupSession` / `cleanupTestProject`) for any resources created before `open()` that cannot yet be tracked via the fixture.

Applied to files:

  • packages/opencode/test/session/loop-gate.test.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-26T16:34:54.895Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 247
File: packages/ui/src/components/message-part.tsx:1322-1324
Timestamp: 2026-04-26T16:34:54.895Z
Learning: In Astro-Han/pawwork (`packages/ui/src/components/message-part.tsx`), the `taskId` createMemo and `childSessionId` createMemo both intentionally read only from `partMetadata().sessionId` (populated post-execution), not from `input.task_id` / `input.subagent_session_id`. This has always been the case — the original code never read the input field either. Adding an `input.subagent_session_id` fallback would be a new capability, not a bug fix. Do NOT flag the absence of this fallback as a regression in PR `#247` or future PRs unless there is a concrete case where metadata is not populated.

Applied to files:

  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-24T05:39:56.086Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 208
File: packages/app/src/components/prompt-input.tsx:1569-1611
Timestamp: 2026-04-24T05:39:56.086Z
Learning: In Astro-Han/pawwork `packages/app/src/components/prompt-input.tsx`, after the composer unification in PR `#208` (fixed in commit 5d810aa):
- `SendButton.disabled` does NOT gate on `store.mode !== "normal"`. Shell mode has a fully visible, clickable orange submit button that calls `handleSubmit` directly (same path as the Enter key in `handleKeyDown`). Do NOT suggest re-adding the mode gate.
- `SendButton` does NOT use the `buttons()` spring opacity animation (`style={buttons()}`). It is always fully visible regardless of mode.
- `WorkspaceChip` is gated on `props.homeMode && store.mode === "normal"` so it hides in shell mode (preventing it from appearing isolated/bright while neighboring controls fade).
- The left-side chip group (`aria-hidden={store.mode !== "normal"}`) covers attach/model/variant/workspace controls only; `SendButton` remains in a separate right-side sibling div.

Applied to files:

  • packages/opencode/src/session/loop-renderer.ts
📚 Learning: 2026-04-24T17:08:44.294Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 224
File: packages/app/src/i18n/zh.ts:0-0
Timestamp: 2026-04-24T17:08:44.294Z
Learning: In Astro-Han/pawwork PR `#224`, the first-occurrence `PawWork 爪印` branding rule originally specified in issue `#196` was superseded by an updated Chinese-branding spec. On all zh UI surfaces in `packages/app/src/i18n/zh.ts` (e.g., `dialog.model.unpaid.freeModels.title`, `session.new.subtitle`, `sidebar.gettingStarted.line1`), the correct and intentional target is fully localized `爪印` branding — no `PawWork` prefix. Do NOT flag these strings as missing the first-occurrence `PawWork 爪印` rule in future reviews.

Applied to files:

  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-22T09:32:58.310Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 126
File: packages/ui/src/theme/context.tsx:11-16
Timestamp: 2026-04-22T09:32:58.310Z
Learning: In Astro-Han/pawwork (`packages/ui/src/theme/context.tsx` and related files), the renaming of localStorage theme keys from `opencode-*` to `pawwork-*` (THEME_ID, COLOR_SCHEME, THEME_CSS_LIGHT, THEME_CSS_DARK) is intentional and should NOT include a migration path from the old keys. Migrating would re-couple PawWork and OpenCode browser storage namespaces, which the PR is explicitly designed to avoid. A reset to the PawWork default theme on upgrade is acceptable by design.

Applied to files:

  • packages/opencode/src/session/loop-renderer.ts
📚 Learning: 2026-04-25T11:52:40.349Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 233
File: packages/desktop-electron/src/main/server.ts:60-60
Timestamp: 2026-04-25T11:52:40.349Z
Learning: In Astro-Han/pawwork (`packages/desktop-electron/src/main/index.ts`), `ensureLoopbackNoProxy()` (defined at line 551) is called inside `setupApp()` at line 263, which runs synchronously before `initialize()` (line 321) and `spawnLocalServer()` (line 395). It unconditionally upserts `127.0.0.1`, `localhost`, and `::1` into both `process.env.NO_PROXY` and `process.env.no_proxy`. This guarantees that `configureProxyDispatcher(process.env)` in `server.ts` always receives a `process.env` that already excludes loopback from proxying. Do not flag the absence of a loopback-merge inside `configureProxyDispatcher` or `normalizeProxyConfig` — the exclusion is already enforced at a higher layer before those functions are ever called.

Applied to files:

  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-23T15:26:07.250Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 193
File: packages/app/src/pages/layout/sidebar-items.tsx:102-107
Timestamp: 2026-04-23T15:26:07.250Z
Learning: In Astro-Han/pawwork (`packages/app/src/pages/layout/sidebar-items.tsx`), the `indicator()` function in `SessionRow` intentionally renders `props.leadingSlot` (the pin button) only as a fallback when no status indicator (running/permission/error/unseen) is active. When a higher-priority status wins the slot, the pin button is removed from the DOM — this is a deliberate design choice for the merged leading slot (`#150`). The keyboard unpin path is preserved via: (1) focusing the row anchor triggers `group-focus-within` which reveals the dots menu trigger, then Tab → Enter → "Unpin Session"; (2) the context menu (right-click / Shift+F10) exposes "Unpin Session". The "always render + CSS overlay" approach was considered but rejected due to z-index/pointer-events complexity; residual `...` slot behavior is tracked in `#192`. Do NOT flag the absence of the pin button from the DOM when a status is active as an accessibility regression.

Applied to files:

  • packages/opencode/src/session/loop-renderer.ts
📚 Learning: 2026-04-26T15:35:31.757Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 245
File: packages/opencode/src/question/index.ts:21-24
Timestamp: 2026-04-26T15:35:31.757Z
Learning: In Astro-Han/pawwork (`packages/opencode/src/question/index.ts`), `Question.Option.description` is intentionally **required** (`z.string()`, not `.optional()`). This matches the upstream opencode contract and all current callers (e.g. `PlanExitTool`) always supply a description. The defensive `<Show when={props.description}>` rendering in `session-question-dock.tsx` is a standard guard, not a signal that the field is intended to be optional. Do NOT suggest making `Option.description` optional without a dedicated follow-up that covers schema + tool description + dock copy + tests.

Applied to files:

  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/session/diagnostics.ts
  • packages/opencode/src/session/processor.ts
📚 Learning: 2026-04-20T14:21:56.373Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 71
File: packages/app/src/components/session/session-status-connections.tsx:146-147
Timestamp: 2026-04-20T14:21:56.373Z
Learning: In the Astro-Han/pawwork repository (SolidJS app), `sync.data.config` is always initialized to `{}` at `packages/app/src/context/global-sync.tsx` line 71 and is never `undefined` at runtime. Non-optional property access like `sync.data.config.plugin` is intentional and consistent with the pattern used in `packages/app/src/components/status-popover-body.tsx` line 243. Do not flag `sync.data.config.plugin` as needing optional chaining.

Applied to files:

  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-21T16:57:25.580Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 102
File: packages/opencode/src/config/agent.ts:108-119
Timestamp: 2026-04-21T16:57:25.580Z
Learning: In `packages/opencode/src/config/agent.ts` (Astro-Han/pawwork), `ConfigPermission.Info` only accepts permission objects or the three action strings `"ask"`, `"allow"`, `"deny"`, and transforms those action strings into `{ "*": action }` before `normalize()` runs. By the time `normalize()` is reached, `configuredPermission` is always either `undefined` or a `Record<string, Rule>` — never a raw arbitrary string. The `Object.assign(permission, configuredPermission)` pattern is therefore safe. Do not flag it as corrupting string permission references.

Applied to files:

  • packages/opencode/src/session/loop-renderer.ts
📚 Learning: 2026-04-27T08:58:00.665Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 264
File: packages/opencode/src/session/prompt.ts:531-538
Timestamp: 2026-04-27T08:58:00.665Z
Learning: When using Effect (e.g., `yield*` with `Effect`-style generator yielding), only use `yield* new SomeErrorClass(...)` if `SomeErrorClass` extends `Schema.TaggedErrorClass` (i.e., it implements Effect’s Yieldable interface). For plain `Error` subclasses (like `BlockedLoopError` / `LoopStopError`) or inline `new Error(...)` values, they are not yieldable and must be wrapped as `yield* Effect.fail(new PlainError(...))`. Do not recommend changing `yield* Effect.fail(new SomePlainError(...))` to `yield* new SomePlainError(...)` unless the error class extends `Schema.TaggedErrorClass`.

Applied to files:

  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/diagnostics.ts
  • packages/opencode/src/session/processor.ts
📚 Learning: 2026-04-22T08:49:47.800Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 126
File: packages/desktop-electron/src/main/index-sidecar-source.test.ts:3-11
Timestamp: 2026-04-22T08:49:47.800Z
Learning: In `packages/desktop-electron/src/main/index-sidecar-source.test.ts` (Astro-Han/pawwork), the test intentionally uses `expect(source).toContain` / `expect(source).not.toContain` string matching against the raw `index.ts` source text as a lightweight sidecar contract guard. The maintainer has explicitly chosen not to introduce an AST parser (e.g., `babel/parser` or acorn) for this purpose. Do not flag these string-based assertions as fragile or suggest converting them to AST-based matching.

Applied to files:

  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-23T15:25:31.118Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 193
File: packages/app/e2e/sidebar/sidebar-leading-slot.spec.ts:5-55
Timestamp: 2026-04-23T15:25:31.118Z
Learning: In Astro-Han/pawwork E2E tests, reaching a real "running" session state requires the `project` fixture (for model bootstrap) plus `llm.wait(1)` orchestration. The bare `sdk` fixture used by `withSession` does not trigger an LLM call even with `agent: "build"` + `system` prompt set via `sdk.session.promptAsync`, so simulating running state is not lightweight in the current test infrastructure.

Applied to files:

  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/src/session/processor.ts
📚 Learning: 2026-04-25T12:52:32.462Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 234
File: packages/desktop-electron/src/main/ipc.ts:238-263
Timestamp: 2026-04-25T12:52:32.462Z
Learning: In Astro-Han/pawwork (`packages/desktop-electron/src/main/ipc.ts`), `deps.getServerReadyData()` (backed by `serverReady.promise` in `index.ts`) resolves once at server startup and remains settled; it is not expected to reject in practice. Do not flag the absence of a try-catch around it in the `export-session` IPC handler — the network/fetch layer in `server-client.ts` already has a 10-second AbortController timeout and returns a typed `{ok: false, error}` payload, covering the real failure modes.

Applied to files:

  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/diagnostics.ts
  • packages/opencode/src/session/processor.ts
📚 Learning: 2026-04-23T08:51:04.230Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 186
File: packages/opencode/test/plugin/workspace-adaptor.test.ts:139-144
Timestamp: 2026-04-23T08:51:04.230Z
Learning: In Astro-Han/pawwork (`packages/opencode/src/util/filesystem.ts`), the `Filesystem` utility does NOT expose a `remove` or `unlink` helper. The established repository pattern for auth.json teardown in tests (e.g. `provider.test.ts`, `amazon-bedrock.test.ts`, `workspace-adaptor.test.ts`) is to combine `Filesystem.write` with `node:fs/promises unlink`. Do not flag this mixed usage as inconsistent — it is the correct and intentional pattern.

Applied to files:

  • packages/opencode/src/session/prompt.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : In `Effect.gen` / `Effect.fn`, prefer `yield* new MyError(...)` over `yield* Effect.fail(new MyError(...))` for direct early-failure branches

Applied to files:

  • packages/opencode/src/session/prompt.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : Use `Schema.Defect` instead of `unknown` for defect-like causes in Effect code

Applied to files:

  • packages/opencode/src/session/prompt.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : Use `Effect.gen(function* () { ... })` for Effect composition

Applied to files:

  • packages/opencode/src/session/prompt.ts
📚 Learning: 2026-04-25T12:52:36.999Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 234
File: packages/opencode/src/session/export.ts:75-88
Timestamp: 2026-04-25T12:52:36.999Z
Learning: In `packages/opencode/src/session/export.ts` (Astro-Han/pawwork), the `extractReasonFromCause` function intentionally uses a lightweight cast-based inspection of the Cause `reasons` array instead of `Cause.failures()` / `Cause.defects()` utilities. This is a deliberate choice: the function is diagnostic-only (used to populate a best-effort reason string for export diagnostics), and the maintainer explicitly prefers not to add coupling to Effect's Cause API surface for this purpose. Do not flag this pattern as fragile or suggest replacing it with Cause utilities.

Applied to files:

  • packages/opencode/src/session/prompt.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : For background loops or scheduled tasks, use `Effect.repeat` or `Effect.schedule` with `Effect.forkScoped` in the layer definition

Applied to files:

  • packages/opencode/src/session/prompt.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : Use `Schema.TaggedErrorClass` for typed errors in Effect schemas

Applied to files:

  • packages/opencode/src/session/prompt.ts
📚 Learning: 2026-04-27T08:58:00.665Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 264
File: packages/opencode/src/session/prompt.ts:531-538
Timestamp: 2026-04-27T08:58:00.665Z
Learning: In `packages/opencode/src/session/prompt.ts` (and the broader Astro-Han/pawwork codebase), `yield* new ErrorClass()` is ONLY valid for classes that extend `Schema.TaggedErrorClass` (which implements Effect's Yieldable interface). Plain `Error` subclasses (e.g. `BlockedLoopError`, `LoopStopError` in prompt.ts, or inline `new Error(...)` instances) are NOT yieldable and MUST be wrapped: `yield* Effect.fail(new PlainError(...))`. Do NOT suggest replacing `yield* Effect.fail(new SomePlainError(...))` with `yield* new SomePlainError(...)` unless the class extends `Schema.TaggedErrorClass`.

Applied to files:

  • packages/opencode/test/session/prompt-effect.test.ts
📚 Learning: 2026-04-24T13:03:10.835Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 222
File: packages/desktop-electron/src/main/index.ts:686-692
Timestamp: 2026-04-24T13:03:10.835Z
Learning: In `packages/desktop-electron/src/main/index.ts`, the `checkForUpdates()` function intentionally uses recursive self-invocation for the "Retry" path in the update-check failure dialog. This is mandated by the v5.2 design spec (`#213`): "Await the retry recursion and log any rejection so support can see repeated failures." Because retries are user-paced (require a button click), all prior async frames have already unwound through microtasks before the next attempt, so there is no stack/frame-nesting problem in practice. Do not suggest refactoring this to an iterative loop.

Applied to files:

  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Use the `tmpdir` function from `fixture/fixture.ts` to create temporary directories for tests with automatic cleanup. Use `await using` syntax to ensure automatic cleanup when the variable goes out of scope.

Applied to files:

  • packages/opencode/test/session/export.test.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : When a test needs instance-local state, prefer `provideTmpdirInstance(...)` or `provideInstance(...)` over manual `Instance.provide(...)` inside Promise-style tests.

Applied to files:

  • packages/opencode/test/session/export.test.ts
📚 Learning: 2026-04-20T14:36:04.113Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/app/e2e/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:04.113Z
Learning: Applies to packages/app/e2e/packages/app/src/testing/**/*.ts : Test-only hooks must be inert unless explicitly enabled and should not add normal-runtime listeners, reactive subscriptions, or per-update allocations

Applied to files:

  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
📚 Learning: 2026-04-24T06:50:04.597Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 211
File: packages/opencode/src/provider/models.ts:291-304
Timestamp: 2026-04-24T06:50:04.597Z
Learning: In `packages/opencode/src/provider/models.ts`, the `getWithVersion()` function intentionally uses an unbounded `while (true)` retry loop. It only retries when `catalogVersion` changes between the pre- and post-`Data()` read, which is tied to refresh races. The maintainer (Astro-Han) explicitly rejected adding a MAX_RETRIES cap because a hard failure on provider state bootstrap (due to an arbitrary retry limit) is a worse failure mode than retrying until the catalog stabilizes. The loop is expected to self-settle once concurrent writes stop.

Applied to files:

  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-24T06:50:02.712Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 211
File: packages/opencode/src/provider/models.ts:113-179
Timestamp: 2026-04-24T06:50:02.712Z
Learning: In Astro-Han/pawwork (`packages/opencode/src/provider/models.ts`), `PublishModel` and `PublishProvider` intentionally duplicate fields from `Model` and `Provider` with looser optionality. The split is a deliberate behavior boundary: `PublishModel`/`PublishProvider` (with `.passthrough()`) are used for lenient publish-time validation of incoming catalogs, while `Model`/`Provider` are used for strict runtime normalization. Do not flag this duplication as a maintenance burden or suggest extracting a shared base schema — the explicit separation is intentional and tied to the models-refresh reliability path introduced in PR `#211`.

Applied to files:

  • packages/opencode/src/session/diagnostics.ts
🪛 Betterleaks (1.1.2)
packages/opencode/test/session/diagnostics.test.ts

[high] 253-253: Discovered a potential authorization token provided in a curl command header, which could compromise the curl accessed resource.

(curl-auth-header)

Comment thread packages/opencode/src/session/diagnostics.ts Outdated
Comment thread packages/opencode/src/session/diagnostics.ts
Comment thread packages/opencode/test/session/diagnostics.test.ts
@Astro-Han Astro-Han force-pushed the claude/issue-229-loop-detection-v1 branch from 0e106cf to 4d8ffb5 Compare April 27, 2026 09:16
@Astro-Han

Copy link
Copy Markdown
Owner Author

Addressing the latest CodeRabbit review (3 inline + 1 outside-diff + 1 duplicate-as-extension):

  • Outside-diff: Gate StructuredOutput after a synthetic stop — Fixed in 5524f1b (autosquashed). createStructuredOutputTool now takes a shouldHalt?: () => boolean callback; the call site in the json_schema branch passes () => handle.hasStopped(handle.message.parentID). After a synthetic stop, an auto-resumed StructuredOutput invocation throws halted by PawWork rather than capturing a structured output, so the json_schema turn cannot reopen.

  • Duplicate (extension): Scrub non-URL secrets in lastError — Fixed in 3806d54 (autosquashed). firstErrorLine output now passes through scrubErrorText which masks URL query/fragment, Bearer/Basic/api-key fragments, quoted strings, and Unix/Windows file paths before truncation. Renderer tests cover each class: /Users/alice/private.txt, C:\Users\bob\secrets.json, quoted "secret-payload", and Bearer abc123-def456.

  • 3 inline minor findings — replied + resolved on the threads (target_repeat enum value, target reminder copy, fixture token).

Tests: 389 across 22 files, 0 fail. tsgo --noEmit clean.

@Astro-Han

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Apr 27, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Thorough review of the loop gate v1. No confirmed P0. Several P1 concerns below.

Design observation: the existing review comment about cross-tool target matching has merit. The intentional per-tool scoping (see observeToolCall's record.tool === input.tool filter and test "cross-tool same target keeps newTarget=true") means webfetch + fetch hitting the same 404 URL never accumulate. Model probing loops are single-tool so this is fine, but worth a comment in queryGateAction.

Test coverage: strong. 10 loop-gate cases, 8 loop-renderer cases, snapshot diagnostics, prompt-effect integration. No gaps in gate-decision branches.

Comment thread packages/opencode/src/session/processor.ts
Comment thread packages/opencode/src/session/processor.ts
Comment thread packages/opencode/src/session/diagnostics.ts
Comment thread packages/opencode/src/session/diagnostics.ts
Comment thread packages/opencode/src/session/loop-renderer.ts Outdated
Comment thread packages/opencode/src/session/loop-renderer.ts
Comment thread packages/opencode/src/session/prompt.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/opencode/src/session/diagnostics.ts (1)

477-492: ⚠️ Potential issue | 🟡 Minor

Don't silently consume unsupported error_repeat reminders.

pending collects every reminder, but this renderer only emits copy for input: and target: keys. A persisted error_repeat reminder will be marked injected and collapse into an empty <system-reminder>, so older sessions lose the recovery hint entirely. Add a fallback message for legacy reminders, or avoid flipping them to injected when no reminder text was rendered.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/opencode/src/session/diagnostics.ts` around lines 477 - 492, The
renderer currently only emits reminders for keys matching "input:" and "target:"
and can return an empty "<system-reminder>" when a legacy "error_repeat"
reminder exists; change the logic in this reminder-rendering block (the code
that builds lines, uses pending, sawInput, sawTarget and returns { text:
lines.join("\n"), parts }) to detect pending entries whose key
startsWith("error_repeat") and, if no other reminder text was added (i.e.,
sawInput and sawTarget are both false), push a concise fallback message into
lines (e.g., guidance about repeated errors and recovery options) so the system
reminder is never empty; alternate option: only mark reminders as
injected/consumed when you actually appended text to lines. Ensure you reference
pending, sawInput, sawTarget, lines, and the returned { text, parts } structure
when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/opencode/src/session/loop-renderer.ts`:
- Around line 74-76: scrubURLsInText currently uses a case-sensitive regex so
URLs with uppercase schemes (e.g., HTTPS://) escape scrubbing; update the regex
in scrubURLsInText to be case-insensitive (add the i flag) while preserving the
global flag so that stripQueryAndFragment is applied to all matches (refer to
scrubURLsInText and stripQueryAndFragment).

In `@packages/opencode/test/session/diagnostics.test.ts`:
- Around line 313-318: The test "LoopAction enum is observe|block|stop only"
should assert against the actual exported enum values rather than a hard-coded
array: read the runtime values from SessionDiagnostics.LoopAction (e.g., via
Object.values(SessionDiagnostics.LoopAction) or the exported enum symbol) and
compare that set to the expected values and length to ensure any newly added
values fail the test; update the test that references
SessionDiagnostics.LoopAction to compute the actual values and assert
equality/length instead of relying solely on the type-annotated constant.

In `@packages/opencode/test/session/export.test.ts`:
- Around line 326-329: The test helper makeAssistantInfo uses Math.random() to
build message IDs which can introduce flakiness; change it to produce
deterministic IDs (e.g., use a fixed suffix, a counter, or a seeded
deterministic generator) so MessageID.make("msg_assistant_"+...) yields the same
value each run; update makeAssistantInfo (and any related fixtures) to use a
stable deterministic suffix instead of Math.random(), keeping the SessionID.make
and MessageID.make calls intact.

In `@packages/opencode/test/session/prompt-effect.test.ts`:
- Around line 560-561: The assertion is brittle because it stringifies the whole
requests object; instead iterate/extract the actual text/input fields from the
captured requests (from llm.inputs into the requests array) and assert against
those values directly (e.g., map requests to the property that holds the
user/tool text and use includes/expect.stringContaining on that array or joined
string). Update the test around the requests variable and the expect call so it
checks the specific text field(s) for "repeated the same tool input 3 times"
rather than relying on JSON.stringify of the entire requests object.

---

Outside diff comments:
In `@packages/opencode/src/session/diagnostics.ts`:
- Around line 477-492: The renderer currently only emits reminders for keys
matching "input:" and "target:" and can return an empty "<system-reminder>" when
a legacy "error_repeat" reminder exists; change the logic in this
reminder-rendering block (the code that builds lines, uses pending, sawInput,
sawTarget and returns { text: lines.join("\n"), parts }) to detect pending
entries whose key startsWith("error_repeat") and, if no other reminder text was
added (i.e., sawInput and sawTarget are both false), push a concise fallback
message into lines (e.g., guidance about repeated errors and recovery options)
so the system reminder is never empty; alternate option: only mark reminders as
injected/consumed when you actually appended text to lines. Ensure you reference
pending, sawInput, sawTarget, lines, and the returned { text, parts } structure
when making the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4a430c9c-ff75-42a6-8882-27f16bd3f61b

📥 Commits

Reviewing files that changed from the base of the PR and between 711e7be and 4d8ffb5.

📒 Files selected for processing (11)
  • packages/opencode/src/session/diagnostics.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/src/session/processor.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/test/session/loop-gate.test.ts
  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: e2e-artifacts
🧰 Additional context used
📓 Path-based instructions (2)
packages/opencode/**/*.ts

📄 CodeRabbit inference engine (packages/opencode/AGENTS.md)

packages/opencode/**/*.ts: Use Effect.gen(function* () { ... }) for Effect composition
Use Effect.fn("Domain.method") for named/traced effects and Effect.fnUntraced for internal helpers; these accept pipeable operators as extra arguments to avoid unnecessary outer .pipe() wrappers
Use Effect.callback for callback-based APIs
Prefer DateTime.nowAsDate over new Date(yield* Clock.currentTimeMillis) when you need a Date in Effect code
Use Schema.Class for multi-field data in Effect schemas
Use branded schemas (Schema.brand) for single-value types in Effect
Use Schema.TaggedErrorClass for typed errors in Effect schemas
Use Schema.Defect instead of unknown for defect-like causes in Effect code
In Effect.gen / Effect.fn, prefer yield* new MyError(...) over yield* Effect.fail(new MyError(...)) for direct early-failure branches
Use makeRuntime from src/effect/run-service.ts for all services; it returns { runPromise, runFork, runCallback } backed by a shared memoMap that deduplicates layers
Use InstanceState from src/effect/instance-state.ts for per-directory or per-project state that needs per-instance cleanup; do work directly in the InstanceState.make closure where ScopedCache handles run-once semantics
Use Effect.addFinalizer or Effect.acquireRelease inside the InstanceState.make closure for cleanup (subscriptions, process teardown, etc.)
Use Effect.forkScoped inside the InstanceState.make closure for background stream consumers — the fiber is interrupted when the instance is disposed
Prefer FileSystem.FileSystem instead of raw fs/promises for effectful file I/O in Effect services
Prefer ChildProcessSpawner.ChildProcessSpawner with ChildProcess.make(...) instead of custom process wrappers in Effect services
Prefer HttpClient.HttpClient instead of raw fetch in Effect services
Prefer Path.Path, Config, Clock, and DateTime services when those concerns are already inside Effect code
For backgroun...

Files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/test/session/loop-gate.test.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/src/session/diagnostics.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/src/session/processor.ts
  • packages/opencode/test/session/diagnostics.test.ts
packages/opencode/test/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (packages/opencode/test/AGENTS.md)

packages/opencode/test/**/*.test.{ts,tsx}: Use the tmpdir function from fixture/fixture.ts to create temporary directories for tests with automatic cleanup. Use await using syntax to ensure automatic cleanup when the variable goes out of scope.
When using the tmpdir function with git repository support, pass the git: true option to initialize a git repo with a root commit.
Use the config option in tmpdir to write an opencode.json config file during test setup by passing a partial Config.Info object.
Use the init option in tmpdir to define custom setup functions that can return extra data accessible via tmp.extra, and use the dispose option for custom cleanup logic.
Use testEffect(...) from test/lib/effect.ts for tests that exercise Effect services or Effect-based workflows.
Use it.effect(...) when the test should run with TestClock and TestConsole. Use it.live(...) when the test depends on real time, filesystem mtimes, child processes, git, locks, or other live OS behavior.
Prefer Effect-aware helpers from fixture/fixture.ts over building manual runtimes in tests: use tmpdirScoped() for scoped temp directories, provideInstance(dir)(effect) for low-level binding without directory creation, provideTmpdirInstance(...) for single temp instance binding, or provideTmpdirServer(...) for tests that also need the test LLM server.
Define const it = testEffect(...) near the top of the test file and keep the test body inside Effect.gen(function* () { ... }). Yield services directly with yield* MyService.Service or yield* MyTool.
Avoid custom ManagedRuntime, attach(...), or ad hoc run(...) wrappers in Effect tests when testEffect(...) already provides the runtime.
When a test needs instance-local state, prefer provideTmpdirInstance(...) or provideInstance(...) over manual Instance.provide(...) inside Promise-style tests.

Files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/test/session/loop-gate.test.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
🧠 Learnings (41)
📓 Common learnings
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 264
File: packages/opencode/src/session/diagnostics.ts:255-279
Timestamp: 2026-04-27T08:27:40.791Z
Learning: In `packages/opencode/src/session/diagnostics.ts` (PR `#264`, issue `#229`), target-level loop detection is intentionally tool-scoped. The `real` filter includes `r.tool === input.tool` and target signature keys are formatted as `target:${tool}:${targetHash}`. Cross-tool retries on the same URL/path/query are considered legitimate exploration (e.g., webfetch failed → switch to a different tool), NOT a stuck loop. Only same-tool + same-target accumulation counts toward block/stop. Do NOT suggest removing the tool scope from target signature keys or the `real` filter.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 206
File: packages/app/e2e/prompt/prompt-footer-focus.spec.ts:131-143
Timestamp: 2026-04-24T03:51:54.050Z
Learning: In Astro-Han/pawwork E2E tests (packages/app/e2e/fixtures.ts), `project.prompt(text)` internally calls `trackSession(next.sessionID, active.directory)` after the prompt submission is observed and the active session is resolved. Tests that obtain a `sessionID` from `project.prompt()` do NOT need to call `project.trackSession(sessionID)` manually — it is already registered for teardown. Calling it again would be duplicate ownership.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 224
File: packages/app/src/i18n/zh.ts:0-0
Timestamp: 2026-04-24T17:08:44.294Z
Learning: In Astro-Han/pawwork PR `#224`, the first-occurrence `PawWork 爪印` branding rule originally specified in issue `#196` was superseded by an updated Chinese-branding spec. On all zh UI surfaces in `packages/app/src/i18n/zh.ts` (e.g., `dialog.model.unpaid.freeModels.title`, `session.new.subtitle`, `sidebar.gettingStarted.line1`), the correct and intentional target is fully localized `爪印` branding — no `PawWork` prefix. Do NOT flag these strings as missing the first-occurrence `PawWork 爪印` rule in future reviews.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 247
File: packages/ui/src/components/message-part.tsx:1322-1324
Timestamp: 2026-04-26T16:34:54.895Z
Learning: In Astro-Han/pawwork (`packages/ui/src/components/message-part.tsx`), the `taskId` createMemo and `childSessionId` createMemo both intentionally read only from `partMetadata().sessionId` (populated post-execution), not from `input.task_id` / `input.subagent_session_id`. This has always been the case — the original code never read the input field either. Adding an `input.subagent_session_id` fallback would be a new capability, not a bug fix. Do NOT flag the absence of this fallback as a regression in PR `#247` or future PRs unless there is a concrete case where metadata is not populated.
📚 Learning: 2026-04-24T03:51:54.050Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 206
File: packages/app/e2e/prompt/prompt-footer-focus.spec.ts:131-143
Timestamp: 2026-04-24T03:51:54.050Z
Learning: In Astro-Han/pawwork E2E tests (packages/app/e2e/fixtures.ts), `project.prompt(text)` internally calls `trackSession(next.sessionID, active.directory)` after the prompt submission is observed and the active session is resolved. Tests that obtain a `sessionID` from `project.prompt()` do NOT need to call `project.trackSession(sessionID)` manually — it is already registered for teardown. Calling it again would be duplicate ownership.

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/src/session/diagnostics.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
📚 Learning: 2026-04-27T08:27:40.791Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 264
File: packages/opencode/src/session/diagnostics.ts:255-279
Timestamp: 2026-04-27T08:27:40.791Z
Learning: In `packages/opencode/src/session/diagnostics.ts` (PR `#264`, issue `#229`), target-level loop detection is intentionally tool-scoped. The `real` filter includes `r.tool === input.tool` and target signature keys are formatted as `target:${tool}:${targetHash}`. Cross-tool retries on the same URL/path/query are considered legitimate exploration (e.g., webfetch failed → switch to a different tool), NOT a stuck loop. Only same-tool + same-target accumulation counts toward block/stop. Do NOT suggest removing the tool scope from target signature keys or the `real` filter.

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/test/session/loop-gate.test.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/src/session/diagnostics.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/src/session/processor.ts
  • packages/opencode/test/session/diagnostics.test.ts
📚 Learning: 2026-04-20T14:36:04.113Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/app/e2e/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:04.113Z
Learning: Applies to packages/app/e2e/**/*.spec.ts : Use fixture-managed cleanup with `withSession(sdk, title, callback)` for temporary sessions instead of calling `sdk.session.delete(...)` directly

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
📚 Learning: 2026-04-26T16:34:54.895Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 247
File: packages/ui/src/components/message-part.tsx:1322-1324
Timestamp: 2026-04-26T16:34:54.895Z
Learning: In Astro-Han/pawwork (`packages/ui/src/components/message-part.tsx`), the `taskId` createMemo and `childSessionId` createMemo both intentionally read only from `partMetadata().sessionId` (populated post-execution), not from `input.task_id` / `input.subagent_session_id`. This has always been the case — the original code never read the input field either. Adding an `input.subagent_session_id` fallback would be a new capability, not a bug fix. Do NOT flag the absence of this fallback as a regression in PR `#247` or future PRs unless there is a concrete case where metadata is not populated.

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/src/session/diagnostics.ts
  • packages/opencode/test/session/diagnostics.test.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Define `const it = testEffect(...)` near the top of the test file and keep the test body inside `Effect.gen(function* () { ... })`. Yield services directly with `yield* MyService.Service` or `yield* MyTool`.

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/test/session/loop-gate.test.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/src/session/processor.ts
  • packages/opencode/test/session/diagnostics.test.ts
📚 Learning: 2026-04-23T08:51:00.819Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 186
File: packages/opencode/test/plugin/workspace-adaptor.test.ts:139-144
Timestamp: 2026-04-23T08:51:00.819Z
Learning: For pawwork tests under packages/opencode/test/**, auth.json teardown may intentionally combine `Filesystem.write` (from `packages/opencode/src/util/filesystem.ts`) with `node:fs/promises` `unlink` for cleanup. Do not flag this as inconsistent style; it is the established/intentional pattern because `Filesystem` does not provide a `remove`/`unlink` helper.

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/test/session/loop-gate.test.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
📚 Learning: 2026-04-22T09:32:54.556Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 126
File: packages/opencode/test/provider/provider.test.ts:64-85
Timestamp: 2026-04-22T09:32:54.556Z
Learning: In `packages/opencode/test/provider/provider.test.ts`, the file intentionally uses AppRuntime-based async helpers (`run`, `list`, `getProvider`, etc.) rather than `testEffect(...)` for all tests. Converting individual tests to `testEffect` while leaving the rest on the async pattern would create internal inconsistency. A full harness migration of this file is the right approach if the pattern needs to change, but that should be a separate PR.

Applied to files:

  • packages/opencode/test/session/loop-renderer.test.ts
📚 Learning: 2026-04-20T14:36:04.113Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/app/e2e/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:04.113Z
Learning: Applies to packages/app/e2e/packages/app/src/testing/**/*.ts : Test-only hooks must be inert unless explicitly enabled and should not add normal-runtime listeners, reactive subscriptions, or per-update allocations

Applied to files:

  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
📚 Learning: 2026-04-24T05:39:56.086Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 208
File: packages/app/src/components/prompt-input.tsx:1569-1611
Timestamp: 2026-04-24T05:39:56.086Z
Learning: In Astro-Han/pawwork `packages/app/src/components/prompt-input.tsx`, after the composer unification in PR `#208` (fixed in commit 5d810aa):
- `SendButton.disabled` does NOT gate on `store.mode !== "normal"`. Shell mode has a fully visible, clickable orange submit button that calls `handleSubmit` directly (same path as the Enter key in `handleKeyDown`). Do NOT suggest re-adding the mode gate.
- `SendButton` does NOT use the `buttons()` spring opacity animation (`style={buttons()}`). It is always fully visible regardless of mode.
- `WorkspaceChip` is gated on `props.homeMode && store.mode === "normal"` so it hides in shell mode (preventing it from appearing isolated/bright while neighboring controls fade).
- The left-side chip group (`aria-hidden={store.mode !== "normal"}`) covers attach/model/variant/workspace controls only; `SendButton` remains in a separate right-side sibling div.

Applied to files:

  • packages/opencode/src/session/loop-renderer.ts
📚 Learning: 2026-04-24T17:08:44.294Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 224
File: packages/app/src/i18n/zh.ts:0-0
Timestamp: 2026-04-24T17:08:44.294Z
Learning: In Astro-Han/pawwork PR `#224`, the first-occurrence `PawWork 爪印` branding rule originally specified in issue `#196` was superseded by an updated Chinese-branding spec. On all zh UI surfaces in `packages/app/src/i18n/zh.ts` (e.g., `dialog.model.unpaid.freeModels.title`, `session.new.subtitle`, `sidebar.gettingStarted.line1`), the correct and intentional target is fully localized `爪印` branding — no `PawWork` prefix. Do NOT flag these strings as missing the first-occurrence `PawWork 爪印` rule in future reviews.

Applied to files:

  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/src/session/diagnostics.ts
  • packages/opencode/test/session/diagnostics.test.ts
📚 Learning: 2026-04-22T09:32:58.310Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 126
File: packages/ui/src/theme/context.tsx:11-16
Timestamp: 2026-04-22T09:32:58.310Z
Learning: In Astro-Han/pawwork (`packages/ui/src/theme/context.tsx` and related files), the renaming of localStorage theme keys from `opencode-*` to `pawwork-*` (THEME_ID, COLOR_SCHEME, THEME_CSS_LIGHT, THEME_CSS_DARK) is intentional and should NOT include a migration path from the old keys. Migrating would re-couple PawWork and OpenCode browser storage namespaces, which the PR is explicitly designed to avoid. A reset to the PawWork default theme on upgrade is acceptable by design.

Applied to files:

  • packages/opencode/src/session/loop-renderer.ts
📚 Learning: 2026-04-25T11:52:40.349Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 233
File: packages/desktop-electron/src/main/server.ts:60-60
Timestamp: 2026-04-25T11:52:40.349Z
Learning: In Astro-Han/pawwork (`packages/desktop-electron/src/main/index.ts`), `ensureLoopbackNoProxy()` (defined at line 551) is called inside `setupApp()` at line 263, which runs synchronously before `initialize()` (line 321) and `spawnLocalServer()` (line 395). It unconditionally upserts `127.0.0.1`, `localhost`, and `::1` into both `process.env.NO_PROXY` and `process.env.no_proxy`. This guarantees that `configureProxyDispatcher(process.env)` in `server.ts` always receives a `process.env` that already excludes loopback from proxying. Do not flag the absence of a loopback-merge inside `configureProxyDispatcher` or `normalizeProxyConfig` — the exclusion is already enforced at a higher layer before those functions are ever called.

Applied to files:

  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/diagnostics.ts
  • packages/opencode/test/session/diagnostics.test.ts
📚 Learning: 2026-04-23T15:26:07.250Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 193
File: packages/app/src/pages/layout/sidebar-items.tsx:102-107
Timestamp: 2026-04-23T15:26:07.250Z
Learning: In Astro-Han/pawwork (`packages/app/src/pages/layout/sidebar-items.tsx`), the `indicator()` function in `SessionRow` intentionally renders `props.leadingSlot` (the pin button) only as a fallback when no status indicator (running/permission/error/unseen) is active. When a higher-priority status wins the slot, the pin button is removed from the DOM — this is a deliberate design choice for the merged leading slot (`#150`). The keyboard unpin path is preserved via: (1) focusing the row anchor triggers `group-focus-within` which reveals the dots menu trigger, then Tab → Enter → "Unpin Session"; (2) the context menu (right-click / Shift+F10) exposes "Unpin Session". The "always render + CSS overlay" approach was considered but rejected due to z-index/pointer-events complexity; residual `...` slot behavior is tracked in `#192`. Do NOT flag the absence of the pin button from the DOM when a status is active as an accessibility regression.

Applied to files:

  • packages/opencode/src/session/loop-renderer.ts
📚 Learning: 2026-04-26T15:35:31.757Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 245
File: packages/opencode/src/question/index.ts:21-24
Timestamp: 2026-04-26T15:35:31.757Z
Learning: In Astro-Han/pawwork (`packages/opencode/src/question/index.ts`), `Question.Option.description` is intentionally **required** (`z.string()`, not `.optional()`). This matches the upstream opencode contract and all current callers (e.g. `PlanExitTool`) always supply a description. The defensive `<Show when={props.description}>` rendering in `session-question-dock.tsx` is a standard guard, not a signal that the field is intended to be optional. Do NOT suggest making `Option.description` optional without a dedicated follow-up that covers schema + tool description + dock copy + tests.

Applied to files:

  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/src/session/diagnostics.ts
  • packages/opencode/src/session/processor.ts
📚 Learning: 2026-04-20T14:21:56.373Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 71
File: packages/app/src/components/session/session-status-connections.tsx:146-147
Timestamp: 2026-04-20T14:21:56.373Z
Learning: In the Astro-Han/pawwork repository (SolidJS app), `sync.data.config` is always initialized to `{}` at `packages/app/src/context/global-sync.tsx` line 71 and is never `undefined` at runtime. Non-optional property access like `sync.data.config.plugin` is intentional and consistent with the pattern used in `packages/app/src/components/status-popover-body.tsx` line 243. Do not flag `sync.data.config.plugin` as needing optional chaining.

Applied to files:

  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-21T16:57:25.580Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 102
File: packages/opencode/src/config/agent.ts:108-119
Timestamp: 2026-04-21T16:57:25.580Z
Learning: In `packages/opencode/src/config/agent.ts` (Astro-Han/pawwork), `ConfigPermission.Info` only accepts permission objects or the three action strings `"ask"`, `"allow"`, `"deny"`, and transforms those action strings into `{ "*": action }` before `normalize()` runs. By the time `normalize()` is reached, `configuredPermission` is always either `undefined` or a `Record<string, Rule>` — never a raw arbitrary string. The `Object.assign(permission, configuredPermission)` pattern is therefore safe. Do not flag it as corrupting string permission references.

Applied to files:

  • packages/opencode/src/session/loop-renderer.ts
📚 Learning: 2026-04-27T08:58:00.665Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 264
File: packages/opencode/src/session/prompt.ts:531-538
Timestamp: 2026-04-27T08:58:00.665Z
Learning: When using Effect (e.g., `yield*` with `Effect`-style generator yielding), only use `yield* new SomeErrorClass(...)` if `SomeErrorClass` extends `Schema.TaggedErrorClass` (i.e., it implements Effect’s Yieldable interface). For plain `Error` subclasses (like `BlockedLoopError` / `LoopStopError`) or inline `new Error(...)` values, they are not yieldable and must be wrapped as `yield* Effect.fail(new PlainError(...))`. Do not recommend changing `yield* Effect.fail(new SomePlainError(...))` to `yield* new SomePlainError(...)` unless the error class extends `Schema.TaggedErrorClass`.

Applied to files:

  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/src/session/diagnostics.ts
  • packages/opencode/src/session/processor.ts
📚 Learning: 2026-04-22T08:49:47.800Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 126
File: packages/desktop-electron/src/main/index-sidecar-source.test.ts:3-11
Timestamp: 2026-04-22T08:49:47.800Z
Learning: In `packages/desktop-electron/src/main/index-sidecar-source.test.ts` (Astro-Han/pawwork), the test intentionally uses `expect(source).toContain` / `expect(source).not.toContain` string matching against the raw `index.ts` source text as a lightweight sidecar contract guard. The maintainer has explicitly chosen not to introduce an AST parser (e.g., `babel/parser` or acorn) for this purpose. Do not flag these string-based assertions as fragile or suggest converting them to AST-based matching.

Applied to files:

  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/src/session/diagnostics.ts
  • packages/opencode/test/session/diagnostics.test.ts
📚 Learning: 2026-04-25T12:52:47.074Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 234
File: packages/opencode/src/share/session.ts:27-27
Timestamp: 2026-04-25T12:52:47.074Z
Learning: In `packages/opencode/src/share/session.ts`, the local `ensureEnabled` closure is intentionally duplicated from `ShareRuntime.ensureEnabled` in `runtime.ts`. Using the shared `ShareRuntime.ensureEnabled` effect directly would leak `CloudShareGate` back into the `R` (requirement) type of `share`, `unshare`, and `create`, because the shared effect re-yields the service rather than capturing an already-resolved gate reference. The current pattern resolves `gate` once at the top of the outer `Effect.gen` and then closes over it in a local `ensureEnabled`, keeping the inner effects' requirement types clean. A comment in the file points to `runtime.ts` to make the intentional duplication discoverable. The real fix would be refactoring `ensureEnabled` to accept a gate parameter, but that change is larger than the DRY benefit warrants. Do not flag this duplication as a maintenance issue.

Applied to files:

  • packages/opencode/test/session/loop-gate.test.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Prefer Effect-aware helpers from `fixture/fixture.ts` over building manual runtimes in tests: use `tmpdirScoped()` for scoped temp directories, `provideInstance(dir)(effect)` for low-level binding without directory creation, `provideTmpdirInstance(...)` for single temp instance binding, or `provideTmpdirServer(...)` for tests that also need the test LLM server.

Applied to files:

  • packages/opencode/test/session/export.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : When a test needs instance-local state, prefer `provideTmpdirInstance(...)` or `provideInstance(...)` over manual `Instance.provide(...)` inside Promise-style tests.

Applied to files:

  • packages/opencode/test/session/export.test.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Use the `tmpdir` function from `fixture/fixture.ts` to create temporary directories for tests with automatic cleanup. Use `await using` syntax to ensure automatic cleanup when the variable goes out of scope.

Applied to files:

  • packages/opencode/test/session/export.test.ts
📚 Learning: 2026-04-24T00:02:53.315Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 203
File: packages/app/e2e/sidebar/sidebar-session-links.spec.ts:34-55
Timestamp: 2026-04-24T00:02:53.315Z
Learning: In Astro-Han/pawwork E2E tests (`packages/app/e2e/**/*.spec.ts`), `project.trackDirectory()` and `project.trackSession()` cannot be called before `project.open()` — the `project` fixture throws until `open()` initializes its internal state. The correct pattern is: call `project.trackSession(sessionID)` from inside the `beforeGoto` callback (where state already exists), call `project.trackDirectory(directory)` and cross-workspace `project.trackSession(id, directory)` immediately after `project.open()` returns, and rely on explicit `finally` cleanup (e.g. `cleanupSession` / `cleanupTestProject`) for any resources created before `open()` that cannot yet be tracked via the fixture.

Applied to files:

  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/src/session/diagnostics.ts
  • packages/opencode/test/session/diagnostics.test.ts
📚 Learning: 2026-04-25T12:52:32.462Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 234
File: packages/desktop-electron/src/main/ipc.ts:238-263
Timestamp: 2026-04-25T12:52:32.462Z
Learning: In Astro-Han/pawwork (`packages/desktop-electron/src/main/ipc.ts`), `deps.getServerReadyData()` (backed by `serverReady.promise` in `index.ts`) resolves once at server startup and remains settled; it is not expected to reject in practice. Do not flag the absence of a try-catch around it in the `export-session` IPC handler — the network/fetch layer in `server-client.ts` already has a 10-second AbortController timeout and returns a typed `{ok: false, error}` payload, covering the real failure modes.

Applied to files:

  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/src/session/diagnostics.ts
  • packages/opencode/src/session/processor.ts
  • packages/opencode/test/session/diagnostics.test.ts
📚 Learning: 2026-04-25T12:52:36.999Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 234
File: packages/opencode/src/session/export.ts:75-88
Timestamp: 2026-04-25T12:52:36.999Z
Learning: In `packages/opencode/src/session/export.ts` (Astro-Han/pawwork), the `extractReasonFromCause` function intentionally uses a lightweight cast-based inspection of the Cause `reasons` array instead of `Cause.failures()` / `Cause.defects()` utilities. This is a deliberate choice: the function is diagnostic-only (used to populate a best-effort reason string for export diagnostics), and the maintainer explicitly prefers not to add coupling to Effect's Cause API surface for this purpose. Do not flag this pattern as fragile or suggest replacing it with Cause utilities.

Applied to files:

  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-24T13:03:10.835Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 222
File: packages/desktop-electron/src/main/index.ts:686-692
Timestamp: 2026-04-24T13:03:10.835Z
Learning: In `packages/desktop-electron/src/main/index.ts`, the `checkForUpdates()` function intentionally uses recursive self-invocation for the "Retry" path in the update-check failure dialog. This is mandated by the v5.2 design spec (`#213`): "Await the retry recursion and log any rejection so support can see repeated failures." Because retries are user-paced (require a button click), all prior async frames have already unwound through microtasks before the next attempt, so there is no stack/frame-nesting problem in practice. Do not suggest refactoring this to an iterative loop.

Applied to files:

  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/diagnostics.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
📚 Learning: 2026-04-23T15:25:31.118Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 193
File: packages/app/e2e/sidebar/sidebar-leading-slot.spec.ts:5-55
Timestamp: 2026-04-23T15:25:31.118Z
Learning: In Astro-Han/pawwork E2E tests, reaching a real "running" session state requires the `project` fixture (for model bootstrap) plus `llm.wait(1)` orchestration. The bare `sdk` fixture used by `withSession` does not trigger an LLM call even with `agent: "build"` + `system` prompt set via `sdk.session.promptAsync`, so simulating running state is not lightweight in the current test infrastructure.

Applied to files:

  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/src/session/processor.ts
  • packages/opencode/test/session/diagnostics.test.ts
📚 Learning: 2026-04-23T08:51:04.230Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 186
File: packages/opencode/test/plugin/workspace-adaptor.test.ts:139-144
Timestamp: 2026-04-23T08:51:04.230Z
Learning: In Astro-Han/pawwork (`packages/opencode/src/util/filesystem.ts`), the `Filesystem` utility does NOT expose a `remove` or `unlink` helper. The established repository pattern for auth.json teardown in tests (e.g. `provider.test.ts`, `amazon-bedrock.test.ts`, `workspace-adaptor.test.ts`) is to combine `Filesystem.write` with `node:fs/promises unlink`. Do not flag this mixed usage as inconsistent — it is the correct and intentional pattern.

Applied to files:

  • packages/opencode/src/session/prompt.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : In `Effect.gen` / `Effect.fn`, prefer `yield* new MyError(...)` over `yield* Effect.fail(new MyError(...))` for direct early-failure branches

Applied to files:

  • packages/opencode/src/session/prompt.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : Use `Schema.Defect` instead of `unknown` for defect-like causes in Effect code

Applied to files:

  • packages/opencode/src/session/prompt.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : Use `Effect.gen(function* () { ... })` for Effect composition

Applied to files:

  • packages/opencode/src/session/prompt.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : For background loops or scheduled tasks, use `Effect.repeat` or `Effect.schedule` with `Effect.forkScoped` in the layer definition

Applied to files:

  • packages/opencode/src/session/prompt.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Avoid custom `ManagedRuntime`, `attach(...)`, or ad hoc `run(...)` wrappers in Effect tests when `testEffect(...)` already provides the runtime.

Applied to files:

  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/prompt-effect.test.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : Use `Schema.TaggedErrorClass` for typed errors in Effect schemas

Applied to files:

  • packages/opencode/src/session/prompt.ts
📚 Learning: 2026-04-24T06:50:04.597Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 211
File: packages/opencode/src/provider/models.ts:291-304
Timestamp: 2026-04-24T06:50:04.597Z
Learning: In `packages/opencode/src/provider/models.ts`, the `getWithVersion()` function intentionally uses an unbounded `while (true)` retry loop. It only retries when `catalogVersion` changes between the pre- and post-`Data()` read, which is tied to refresh races. The maintainer (Astro-Han) explicitly rejected adding a MAX_RETRIES cap because a hard failure on provider state bootstrap (due to an arbitrary retry limit) is a worse failure mode than retrying until the catalog stabilizes. The loop is expected to self-settle once concurrent writes stop.

Applied to files:

  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-24T06:50:02.712Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 211
File: packages/opencode/src/provider/models.ts:113-179
Timestamp: 2026-04-24T06:50:02.712Z
Learning: In Astro-Han/pawwork (`packages/opencode/src/provider/models.ts`), `PublishModel` and `PublishProvider` intentionally duplicate fields from `Model` and `Provider` with looser optionality. The split is a deliberate behavior boundary: `PublishModel`/`PublishProvider` (with `.passthrough()`) are used for lenient publish-time validation of incoming catalogs, while `Model`/`Provider` are used for strict runtime normalization. Do not flag this duplication as a maintenance burden or suggest extracting a shared base schema — the explicit separation is intentional and tied to the models-refresh reliability path introduced in PR `#211`.

Applied to files:

  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-27T08:58:00.665Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 264
File: packages/opencode/src/session/prompt.ts:531-538
Timestamp: 2026-04-27T08:58:00.665Z
Learning: In `packages/opencode/src/session/prompt.ts` (and the broader Astro-Han/pawwork codebase), `yield* new ErrorClass()` is ONLY valid for classes that extend `Schema.TaggedErrorClass` (which implements Effect's Yieldable interface). Plain `Error` subclasses (e.g. `BlockedLoopError`, `LoopStopError` in prompt.ts, or inline `new Error(...)` instances) are NOT yieldable and MUST be wrapped: `yield* Effect.fail(new PlainError(...))`. Do NOT suggest replacing `yield* Effect.fail(new SomePlainError(...))` with `yield* new SomePlainError(...)` unless the class extends `Schema.TaggedErrorClass`.

Applied to files:

  • packages/opencode/test/session/prompt-effect.test.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Use `testEffect(...)` from `test/lib/effect.ts` for tests that exercise Effect services or Effect-based workflows.

Applied to files:

  • packages/opencode/test/session/prompt-effect.test.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Use `it.effect(...)` when the test should run with `TestClock` and `TestConsole`. Use `it.live(...)` when the test depends on real time, filesystem mtimes, child processes, git, locks, or other live OS behavior.

Applied to files:

  • packages/opencode/test/session/prompt-effect.test.ts
📚 Learning: 2026-04-20T17:03:40.214Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 73
File: packages/opencode/src/cli/cmd/tui/context/sync.tsx:486-489
Timestamp: 2026-04-20T17:03:40.214Z
Learning: In Astro-Han/pawwork (`packages/opencode/src/cli/cmd/tui/context/sync.tsx`), `sync.ready` returning `true` when `process.env.OPENCODE_FAST_BOOT` is set is intentional. The plugin-facing data properties `state.config` (initialized to `{}`) and `state.provider` (initialized to `[]`) expose safe-empty defaults, so they are safe to access before bootstrap completes. Do not flag these as needing null-guards or conditional patterns to match `vcs` — the difference is intentional because `vcs` starts as `undefined` while the others have initialized defaults. Changing this would alter the plugin API contract without a concrete failing case.

Applied to files:

  • packages/opencode/test/session/diagnostics.test.ts
🔇 Additional comments (16)
packages/opencode/src/session/processor.ts (1)

300-306: No code change needed. In AI SDK v6, tool-call is always emitted before tool-error, even for input validation failures—validation occurs after the complete tool-call chunk is parsed from the provider stream. By the time tool-error arrives, the part is already in running state, so the early return in failToolCall() for non-running parts is not a concern.

			> Likely an incorrect or invalid review comment.
packages/opencode/test/session/diagnostics.test.ts (9)

43-62: LGTM!

Test correctly validates the behavioral change where reminder firing moved from observeToolCall to observeToolError. The loop iteration and assertion pattern is clean.


126-149: LGTM!

Effective test for error normalization — validates that 504 errors with different request IDs are treated as equivalent, firing recovery only once.


151-152: LGTM!

Clean helper abstractions that improve test readability.


154-254: LGTM!

Thorough test suite covering the key behaviors of observeToolError:

  • Dual firing for input and target on third failure
  • Persistence of lastInput/lastError
  • Correct handling when targetHash is absent
  • Proper reminder type labeling
  • Fallback recomputation when inputHash is undefined

289-299: LGTM!

Good coverage for file-based tools — validates that filePath and filepath variants produce consistent target hashes across read, edit, and write operations.


320-352: LGTM!

Comprehensive edge-case coverage for truncateForRenderer:

  • Basic truncation with ellipsis
  • Codepoint boundary safety (percent-encoding and multibyte)
  • Graceful fallback for non-serializable inputs (circular refs, BigInt)

354-406: LGTM!

Tests correctly validate the tool-scoped target detection design: cross-tool retries on the same URL are treated as legitimate exploration (newTarget=true, targetRepeatCount=1), while same-tool retries accumulate the repeat count. Based on learnings, this is the intentional design.


408-435: LGTM!

Clear validation of targetHashIsFallback behavior — recognized inputs (like URLs for webfetch) produce stable targets, while unknown tool inputs fall back to input hashing.


509-562: LGTM!

Good test for reminder text semantics — validates that target_repeat reminders use "failed against the same target" wording rather than the old "class of tool error" phrasing.

packages/opencode/test/session/compaction.test.ts (1)

159-163: Good mock contract update for loop-gate compatibility.

This keeps the compaction test stub aligned with the expanded SessionProcessor.Handle surface while preserving deterministic behavior.

packages/opencode/test/session/loop-gate.test.ts (1)

35-227: Strong gate-state branch coverage.

The suite cleanly exercises signature state derivation and all key action transitions (observe/block/stop), including the targetHash-absent fallback path.

packages/opencode/test/session/prompt-effect.test.ts (1)

498-559: Excellent end-to-end assertion of block→stop escalation.

This test validates the exact synthetic loop actions, metadata shape, and post-stop behavior in one realistic flow.

packages/opencode/test/session/export.test.ts (1)

183-324: Snapshot diagnostics coverage is now complete for terminal loop actions.

Nice addition of both block and stop paths with timestamp-based precedence checks across tree levels.

packages/opencode/test/session/loop-renderer.test.ts (1)

15-178: Great regression suite for renderer privacy and fallback behavior.

Coverage is comprehensive across URL cleaning, secret/path scrubbing, and truncation branches.

packages/opencode/src/session/loop-renderer.ts (1)

9-48: Renderer flow is clear and defensive.

The branching and fallback handling are clean, and the scrub→truncate pipeline is well-structured for safe user-facing summaries.

Comment thread packages/opencode/src/session/loop-renderer.ts
Comment thread packages/opencode/test/session/diagnostics.test.ts
Comment thread packages/opencode/test/session/export.test.ts
Comment thread packages/opencode/test/session/prompt-effect.test.ts Outdated
@Astro-Han Astro-Han force-pushed the claude/issue-229-loop-detection-v1 branch from 4d8ffb5 to 441e858 Compare April 27, 2026 10:07

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/opencode/src/session/diagnostics.ts`:
- Around line 245-252: The code currently captures raw error text into lastError
(computed where lastInput is set) and later persists it into loop metadata;
instead, create and call a small sanitizer (e.g., sanitizeDiagnostic or
scrubError) and use that sanitized value both when computing lastError and when
storing into loop metadata so raw URLs, auth fragments, query strings, and local
paths are redacted; update uses of lastError (the value computed in the
lastError expression and the metadata assignment that reads it) to pass through
the sanitizer and return only a short, non-sensitive diagnostic hint.

In `@packages/opencode/src/session/loop-renderer.ts`:
- Around line 84-90: The scrubErrorText function currently only normalizes
Windows paths with backslashes and a separate regex excludes patterns containing
':/', so JS-normalized forward-slash Windows paths like "C:/Users/..." slip
through; update scrubErrorText to also replace drive-letter followed by colon
and forward-slash paths (e.g., add or extend the regex handling in
scrubErrorText that currently matches /\b[A-Za-z]:\\[^\s]+/ to also match
/\b[A-Za-z]:\/[^\s]+/ or combine into a single regex handling both separators)
and ensure the subsequent path regex doesn't re-include those; add a
unit/regression test exercising ENOENT messages containing "C:/Users/..." to
verify the scrubbed output contains "<path>".

In `@packages/opencode/src/session/processor.ts`:
- Around line 794-840: recordSyntheticStop currently sets ctx.blocked = true but
SessionProcessor.process only watches ctx.needsCompaction, so the LLM stream
keeps being drained; update SessionProcessor.process to check ctx.blocked (in
addition to ctx.needsCompaction) and immediately stop consuming or ignore any
non-finish stream events once ctx.blocked is true, or alternatively have
recordSyntheticStop set a dedicated flag (e.g., ctx.stopConsuming) and make
SessionProcessor.process bail out of its stream-consumption loop when that flag
is set; ensure the change references the existing symbols recordSyntheticStop,
ctx.blocked (or new ctx.stopConsuming), and SessionProcessor.process so
post-stop events are not appended to the assistant message.

In `@packages/opencode/src/session/prompt.ts`:
- Around line 108-169: The loop-gate state transitions (buildLoopContext +
recordSyntheticBlock/recordSyntheticStop) are not atomic and can race when
multiple tool calls for the same parentID run concurrently; wrap the
decision+record path in a per-parent critical section on SessionProcessor (e.g.,
add an atomic helper like SessionProcessor.runWithLoopLock(parentID, async () =>
{ ... })) so that after re-checking loopCtx.hasStopped/deriving parentLoopState
inside the lock you compute decision and then call
processor.recordSyntheticBlock or processor.recordSyntheticStop atomically;
update the code path in prompt.ts to call the new runWithLoopLock for the
section that computes decision, evaluates sigState, and yields the corresponding
recordSynthetic* operation, and add a regression test that emits two identical
tool calls in the same assistant step and asserts only one synthetic block/stop
is persisted.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 136c4d7e-9164-4c5f-ab4a-02f26dcd7a1c

📥 Commits

Reviewing files that changed from the base of the PR and between 4d8ffb5 and 441e858.

📒 Files selected for processing (11)
  • packages/opencode/src/session/diagnostics.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/src/session/processor.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/test/session/loop-gate.test.ts
  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: smoke-macos-arm64
  • GitHub Check: unit-windows-opencode-server-tools
  • GitHub Check: unit-windows-opencode-session
  • GitHub Check: unit-opencode
  • GitHub Check: unit-windows-desktop
  • GitHub Check: unit-windows-opencode-config-project
  • GitHub Check: unit-windows-app
  • GitHub Check: typecheck
  • GitHub Check: e2e-artifacts
  • GitHub Check: analyze-js-ts
🧰 Additional context used
📓 Path-based instructions (2)
packages/opencode/**/*.ts

📄 CodeRabbit inference engine (packages/opencode/AGENTS.md)

packages/opencode/**/*.ts: Use Effect.gen(function* () { ... }) for Effect composition
Use Effect.fn("Domain.method") for named/traced effects and Effect.fnUntraced for internal helpers; these accept pipeable operators as extra arguments to avoid unnecessary outer .pipe() wrappers
Use Effect.callback for callback-based APIs
Prefer DateTime.nowAsDate over new Date(yield* Clock.currentTimeMillis) when you need a Date in Effect code
Use Schema.Class for multi-field data in Effect schemas
Use branded schemas (Schema.brand) for single-value types in Effect
Use Schema.TaggedErrorClass for typed errors in Effect schemas
Use Schema.Defect instead of unknown for defect-like causes in Effect code
In Effect.gen / Effect.fn, prefer yield* new MyError(...) over yield* Effect.fail(new MyError(...)) for direct early-failure branches
Use makeRuntime from src/effect/run-service.ts for all services; it returns { runPromise, runFork, runCallback } backed by a shared memoMap that deduplicates layers
Use InstanceState from src/effect/instance-state.ts for per-directory or per-project state that needs per-instance cleanup; do work directly in the InstanceState.make closure where ScopedCache handles run-once semantics
Use Effect.addFinalizer or Effect.acquireRelease inside the InstanceState.make closure for cleanup (subscriptions, process teardown, etc.)
Use Effect.forkScoped inside the InstanceState.make closure for background stream consumers — the fiber is interrupted when the instance is disposed
Prefer FileSystem.FileSystem instead of raw fs/promises for effectful file I/O in Effect services
Prefer ChildProcessSpawner.ChildProcessSpawner with ChildProcess.make(...) instead of custom process wrappers in Effect services
Prefer HttpClient.HttpClient instead of raw fetch in Effect services
Prefer Path.Path, Config, Clock, and DateTime services when those concerns are already inside Effect code
For backgroun...

Files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/test/session/loop-gate.test.ts
  • packages/opencode/src/session/processor.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/src/session/diagnostics.ts
packages/opencode/test/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (packages/opencode/test/AGENTS.md)

packages/opencode/test/**/*.test.{ts,tsx}: Use the tmpdir function from fixture/fixture.ts to create temporary directories for tests with automatic cleanup. Use await using syntax to ensure automatic cleanup when the variable goes out of scope.
When using the tmpdir function with git repository support, pass the git: true option to initialize a git repo with a root commit.
Use the config option in tmpdir to write an opencode.json config file during test setup by passing a partial Config.Info object.
Use the init option in tmpdir to define custom setup functions that can return extra data accessible via tmp.extra, and use the dispose option for custom cleanup logic.
Use testEffect(...) from test/lib/effect.ts for tests that exercise Effect services or Effect-based workflows.
Use it.effect(...) when the test should run with TestClock and TestConsole. Use it.live(...) when the test depends on real time, filesystem mtimes, child processes, git, locks, or other live OS behavior.
Prefer Effect-aware helpers from fixture/fixture.ts over building manual runtimes in tests: use tmpdirScoped() for scoped temp directories, provideInstance(dir)(effect) for low-level binding without directory creation, provideTmpdirInstance(...) for single temp instance binding, or provideTmpdirServer(...) for tests that also need the test LLM server.
Define const it = testEffect(...) near the top of the test file and keep the test body inside Effect.gen(function* () { ... }). Yield services directly with yield* MyService.Service or yield* MyTool.
Avoid custom ManagedRuntime, attach(...), or ad hoc run(...) wrappers in Effect tests when testEffect(...) already provides the runtime.
When a test needs instance-local state, prefer provideTmpdirInstance(...) or provideInstance(...) over manual Instance.provide(...) inside Promise-style tests.

Files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/test/session/loop-gate.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
🧠 Learnings (48)
📓 Common learnings
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 264
File: packages/opencode/src/session/diagnostics.ts:255-279
Timestamp: 2026-04-27T08:27:40.791Z
Learning: In `packages/opencode/src/session/diagnostics.ts` (PR `#264`, issue `#229`), target-level loop detection is intentionally tool-scoped. The `real` filter includes `r.tool === input.tool` and target signature keys are formatted as `target:${tool}:${targetHash}`. Cross-tool retries on the same URL/path/query are considered legitimate exploration (e.g., webfetch failed → switch to a different tool), NOT a stuck loop. Only same-tool + same-target accumulation counts toward block/stop. Do NOT suggest removing the tool scope from target signature keys or the `real` filter.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 206
File: packages/app/e2e/prompt/prompt-footer-focus.spec.ts:131-143
Timestamp: 2026-04-24T03:51:54.050Z
Learning: In Astro-Han/pawwork E2E tests (packages/app/e2e/fixtures.ts), `project.prompt(text)` internally calls `trackSession(next.sessionID, active.directory)` after the prompt submission is observed and the active session is resolved. Tests that obtain a `sessionID` from `project.prompt()` do NOT need to call `project.trackSession(sessionID)` manually — it is already registered for teardown. Calling it again would be duplicate ownership.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 224
File: packages/app/src/i18n/zh.ts:0-0
Timestamp: 2026-04-24T17:08:44.294Z
Learning: In Astro-Han/pawwork PR `#224`, the first-occurrence `PawWork 爪印` branding rule originally specified in issue `#196` was superseded by an updated Chinese-branding spec. On all zh UI surfaces in `packages/app/src/i18n/zh.ts` (e.g., `dialog.model.unpaid.freeModels.title`, `session.new.subtitle`, `sidebar.gettingStarted.line1`), the correct and intentional target is fully localized `爪印` branding — no `PawWork` prefix. Do NOT flag these strings as missing the first-occurrence `PawWork 爪印` rule in future reviews.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 264
File: packages/opencode/test/session/diagnostics.test.ts:313-318
Timestamp: 2026-04-27T10:08:52.777Z
Learning: In Astro-Han/pawwork (`packages/opencode/src/session/diagnostics.ts`, PR `#264`), `SessionDiagnostics.LoopAction` is a TypeScript string literal union type (`"observe" | "block" | "stop"`), NOT a runtime enum. There is no runtime form to introspect via `Object.values()`. The test pattern `const all: SessionDiagnostics.LoopAction[] = ["observe", "block", "stop"]; expect(all).toHaveLength(3)` is intentional and correct — it provides compile-time exhaustiveness via the type annotation. Do NOT suggest replacing this with `Object.values()` introspection or flag the runtime length check as insufficient.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 247
File: packages/ui/src/components/message-part.tsx:1322-1324
Timestamp: 2026-04-26T16:34:54.895Z
Learning: In Astro-Han/pawwork (`packages/ui/src/components/message-part.tsx`), the `taskId` createMemo and `childSessionId` createMemo both intentionally read only from `partMetadata().sessionId` (populated post-execution), not from `input.task_id` / `input.subagent_session_id`. This has always been the case — the original code never read the input field either. Adding an `input.subagent_session_id` fallback would be a new capability, not a bug fix. Do NOT flag the absence of this fallback as a regression in PR `#247` or future PRs unless there is a concrete case where metadata is not populated.
📚 Learning: 2026-04-27T08:27:40.791Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 264
File: packages/opencode/src/session/diagnostics.ts:255-279
Timestamp: 2026-04-27T08:27:40.791Z
Learning: In `packages/opencode/src/session/diagnostics.ts` (PR `#264`, issue `#229`), target-level loop detection is intentionally tool-scoped. The `real` filter includes `r.tool === input.tool` and target signature keys are formatted as `target:${tool}:${targetHash}`. Cross-tool retries on the same URL/path/query are considered legitimate exploration (e.g., webfetch failed → switch to a different tool), NOT a stuck loop. Only same-tool + same-target accumulation counts toward block/stop. Do NOT suggest removing the tool scope from target signature keys or the `real` filter.

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/test/session/loop-gate.test.ts
  • packages/opencode/src/session/processor.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-20T14:36:04.113Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/app/e2e/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:04.113Z
Learning: Applies to packages/app/e2e/**/*.spec.ts : Use fixture-managed cleanup with `withSession(sdk, title, callback)` for temporary sessions instead of calling `sdk.session.delete(...)` directly

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/export.test.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Prefer Effect-aware helpers from `fixture/fixture.ts` over building manual runtimes in tests: use `tmpdirScoped()` for scoped temp directories, `provideInstance(dir)(effect)` for low-level binding without directory creation, `provideTmpdirInstance(...)` for single temp instance binding, or `provideTmpdirServer(...)` for tests that also need the test LLM server.

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
📚 Learning: 2026-04-24T03:51:54.050Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 206
File: packages/app/e2e/prompt/prompt-footer-focus.spec.ts:131-143
Timestamp: 2026-04-24T03:51:54.050Z
Learning: In Astro-Han/pawwork E2E tests (packages/app/e2e/fixtures.ts), `project.prompt(text)` internally calls `trackSession(next.sessionID, active.directory)` after the prompt submission is observed and the active session is resolved. Tests that obtain a `sessionID` from `project.prompt()` do NOT need to call `project.trackSession(sessionID)` manually — it is already registered for teardown. Calling it again would be duplicate ownership.

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/test/session/loop-gate.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-27T10:08:52.777Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 264
File: packages/opencode/test/session/diagnostics.test.ts:313-318
Timestamp: 2026-04-27T10:08:52.777Z
Learning: In Astro-Han/pawwork (`packages/opencode/src/session/diagnostics.ts`, PR `#264`), `SessionDiagnostics.LoopAction` is a TypeScript string literal union type (`"observe" | "block" | "stop"`), NOT a runtime enum. There is no runtime form to introspect via `Object.values()`. The test pattern `const all: SessionDiagnostics.LoopAction[] = ["observe", "block", "stop"]; expect(all).toHaveLength(3)` is intentional and correct — it provides compile-time exhaustiveness via the type annotation. Do NOT suggest replacing this with `Object.values()` introspection or flag the runtime length check as insufficient.

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/test/session/loop-gate.test.ts
  • packages/opencode/src/session/processor.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-20T14:36:04.113Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/app/e2e/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:04.113Z
Learning: Applies to packages/app/e2e/**/*.spec.ts : Call `project.trackSession(sessionID, directory?)` and `project.trackDirectory(directory)` for any resources created outside the fixture so teardown can clean them up

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Define `const it = testEffect(...)` near the top of the test file and keep the test body inside `Effect.gen(function* () { ... })`. Yield services directly with `yield* MyService.Service` or `yield* MyTool`.

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/loop-gate.test.ts
  • packages/opencode/src/session/processor.ts
  • packages/opencode/test/session/diagnostics.test.ts
📚 Learning: 2026-04-22T09:32:54.556Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 126
File: packages/opencode/test/provider/provider.test.ts:64-85
Timestamp: 2026-04-22T09:32:54.556Z
Learning: In `packages/opencode/test/provider/provider.test.ts`, the file intentionally uses AppRuntime-based async helpers (`run`, `list`, `getProvider`, etc.) rather than `testEffect(...)` for all tests. Converting individual tests to `testEffect` while leaving the rest on the async pattern would create internal inconsistency. A full harness migration of this file is the right approach if the pattern needs to change, but that should be a separate PR.

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/loop-renderer.test.ts
📚 Learning: 2026-04-23T08:51:00.819Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 186
File: packages/opencode/test/plugin/workspace-adaptor.test.ts:139-144
Timestamp: 2026-04-23T08:51:00.819Z
Learning: For pawwork tests under packages/opencode/test/**, auth.json teardown may intentionally combine `Filesystem.write` (from `packages/opencode/src/util/filesystem.ts`) with `node:fs/promises` `unlink` for cleanup. Do not flag this as inconsistent style; it is the established/intentional pattern because `Filesystem` does not provide a `remove`/`unlink` helper.

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/test/session/loop-gate.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
📚 Learning: 2026-04-22T08:49:47.800Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 126
File: packages/desktop-electron/src/main/index-sidecar-source.test.ts:3-11
Timestamp: 2026-04-22T08:49:47.800Z
Learning: In `packages/desktop-electron/src/main/index-sidecar-source.test.ts` (Astro-Han/pawwork), the test intentionally uses `expect(source).toContain` / `expect(source).not.toContain` string matching against the raw `index.ts` source text as a lightweight sidecar contract guard. The maintainer has explicitly chosen not to introduce an AST parser (e.g., `babel/parser` or acorn) for this purpose. Do not flag these string-based assertions as fragile or suggest converting them to AST-based matching.

Applied to files:

  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-20T14:36:04.113Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/app/e2e/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:04.113Z
Learning: Applies to packages/app/e2e/packages/app/src/testing/**/*.ts : Test-only hooks must be inert unless explicitly enabled and should not add normal-runtime listeners, reactive subscriptions, or per-update allocations

Applied to files:

  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Use the `tmpdir` function from `fixture/fixture.ts` to create temporary directories for tests with automatic cleanup. Use `await using` syntax to ensure automatic cleanup when the variable goes out of scope.

Applied to files:

  • packages/opencode/test/session/export.test.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : When a test needs instance-local state, prefer `provideTmpdirInstance(...)` or `provideInstance(...)` over manual `Instance.provide(...)` inside Promise-style tests.

Applied to files:

  • packages/opencode/test/session/export.test.ts
📚 Learning: 2026-04-26T16:34:54.895Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 247
File: packages/ui/src/components/message-part.tsx:1322-1324
Timestamp: 2026-04-26T16:34:54.895Z
Learning: In Astro-Han/pawwork (`packages/ui/src/components/message-part.tsx`), the `taskId` createMemo and `childSessionId` createMemo both intentionally read only from `partMetadata().sessionId` (populated post-execution), not from `input.task_id` / `input.subagent_session_id`. This has always been the case — the original code never read the input field either. Adding an `input.subagent_session_id` fallback would be a new capability, not a bug fix. Do NOT flag the absence of this fallback as a regression in PR `#247` or future PRs unless there is a concrete case where metadata is not populated.

Applied to files:

  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-24T17:08:44.294Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 224
File: packages/app/src/i18n/zh.ts:0-0
Timestamp: 2026-04-24T17:08:44.294Z
Learning: In Astro-Han/pawwork PR `#224`, the first-occurrence `PawWork 爪印` branding rule originally specified in issue `#196` was superseded by an updated Chinese-branding spec. On all zh UI surfaces in `packages/app/src/i18n/zh.ts` (e.g., `dialog.model.unpaid.freeModels.title`, `session.new.subtitle`, `sidebar.gettingStarted.line1`), the correct and intentional target is fully localized `爪印` branding — no `PawWork` prefix. Do NOT flag these strings as missing the first-occurrence `PawWork 爪印` rule in future reviews.

Applied to files:

  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-25T11:52:40.349Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 233
File: packages/desktop-electron/src/main/server.ts:60-60
Timestamp: 2026-04-25T11:52:40.349Z
Learning: In Astro-Han/pawwork (`packages/desktop-electron/src/main/index.ts`), `ensureLoopbackNoProxy()` (defined at line 551) is called inside `setupApp()` at line 263, which runs synchronously before `initialize()` (line 321) and `spawnLocalServer()` (line 395). It unconditionally upserts `127.0.0.1`, `localhost`, and `::1` into both `process.env.NO_PROXY` and `process.env.no_proxy`. This guarantees that `configureProxyDispatcher(process.env)` in `server.ts` always receives a `process.env` that already excludes loopback from proxying. Do not flag the absence of a loopback-merge inside `configureProxyDispatcher` or `normalizeProxyConfig` — the exclusion is already enforced at a higher layer before those functions are ever called.

Applied to files:

  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-24T00:02:53.315Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 203
File: packages/app/e2e/sidebar/sidebar-session-links.spec.ts:34-55
Timestamp: 2026-04-24T00:02:53.315Z
Learning: In Astro-Han/pawwork E2E tests (`packages/app/e2e/**/*.spec.ts`), `project.trackDirectory()` and `project.trackSession()` cannot be called before `project.open()` — the `project` fixture throws until `open()` initializes its internal state. The correct pattern is: call `project.trackSession(sessionID)` from inside the `beforeGoto` callback (where state already exists), call `project.trackDirectory(directory)` and cross-workspace `project.trackSession(id, directory)` immediately after `project.open()` returns, and rely on explicit `finally` cleanup (e.g. `cleanupSession` / `cleanupTestProject`) for any resources created before `open()` that cannot yet be tracked via the fixture.

Applied to files:

  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/test/session/loop-gate.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-26T15:35:31.757Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 245
File: packages/opencode/src/question/index.ts:21-24
Timestamp: 2026-04-26T15:35:31.757Z
Learning: In Astro-Han/pawwork (`packages/opencode/src/question/index.ts`), `Question.Option.description` is intentionally **required** (`z.string()`, not `.optional()`). This matches the upstream opencode contract and all current callers (e.g. `PlanExitTool`) always supply a description. The defensive `<Show when={props.description}>` rendering in `session-question-dock.tsx` is a standard guard, not a signal that the field is intended to be optional. Do NOT suggest making `Option.description` optional without a dedicated follow-up that covers schema + tool description + dock copy + tests.

Applied to files:

  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/src/session/processor.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-20T14:21:56.373Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 71
File: packages/app/src/components/session/session-status-connections.tsx:146-147
Timestamp: 2026-04-20T14:21:56.373Z
Learning: In the Astro-Han/pawwork repository (SolidJS app), `sync.data.config` is always initialized to `{}` at `packages/app/src/context/global-sync.tsx` line 71 and is never `undefined` at runtime. Non-optional property access like `sync.data.config.plugin` is intentional and consistent with the pattern used in `packages/app/src/components/status-popover-body.tsx` line 243. Do not flag `sync.data.config.plugin` as needing optional chaining.

Applied to files:

  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/session/loop-renderer.ts
📚 Learning: 2026-04-20T14:36:04.113Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/app/e2e/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:04.113Z
Learning: Applies to packages/app/e2e/**/*.spec.ts : Prefer fluent helpers and drivers when they make intent obvious and reduce locator-heavy noise in tests

Applied to files:

  • packages/opencode/test/session/export.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts
📚 Learning: 2026-04-20T14:36:04.113Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/app/e2e/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:04.113Z
Learning: Applies to packages/app/e2e/**/*.spec.ts : Import test utilities from `../fixtures` instead of `playwright/test`

Applied to files:

  • packages/opencode/test/session/export.test.ts
📚 Learning: 2026-04-24T05:39:56.086Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 208
File: packages/app/src/components/prompt-input.tsx:1569-1611
Timestamp: 2026-04-24T05:39:56.086Z
Learning: In Astro-Han/pawwork `packages/app/src/components/prompt-input.tsx`, after the composer unification in PR `#208` (fixed in commit 5d810aa):
- `SendButton.disabled` does NOT gate on `store.mode !== "normal"`. Shell mode has a fully visible, clickable orange submit button that calls `handleSubmit` directly (same path as the Enter key in `handleKeyDown`). Do NOT suggest re-adding the mode gate.
- `SendButton` does NOT use the `buttons()` spring opacity animation (`style={buttons()}`). It is always fully visible regardless of mode.
- `WorkspaceChip` is gated on `props.homeMode && store.mode === "normal"` so it hides in shell mode (preventing it from appearing isolated/bright while neighboring controls fade).
- The left-side chip group (`aria-hidden={store.mode !== "normal"}`) covers attach/model/variant/workspace controls only; `SendButton` remains in a separate right-side sibling div.

Applied to files:

  • packages/opencode/src/session/loop-renderer.ts
📚 Learning: 2026-04-22T09:32:58.310Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 126
File: packages/ui/src/theme/context.tsx:11-16
Timestamp: 2026-04-22T09:32:58.310Z
Learning: In Astro-Han/pawwork (`packages/ui/src/theme/context.tsx` and related files), the renaming of localStorage theme keys from `opencode-*` to `pawwork-*` (THEME_ID, COLOR_SCHEME, THEME_CSS_LIGHT, THEME_CSS_DARK) is intentional and should NOT include a migration path from the old keys. Migrating would re-couple PawWork and OpenCode browser storage namespaces, which the PR is explicitly designed to avoid. A reset to the PawWork default theme on upgrade is acceptable by design.

Applied to files:

  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/src/session/prompt.ts
📚 Learning: 2026-04-23T15:26:07.250Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 193
File: packages/app/src/pages/layout/sidebar-items.tsx:102-107
Timestamp: 2026-04-23T15:26:07.250Z
Learning: In Astro-Han/pawwork (`packages/app/src/pages/layout/sidebar-items.tsx`), the `indicator()` function in `SessionRow` intentionally renders `props.leadingSlot` (the pin button) only as a fallback when no status indicator (running/permission/error/unseen) is active. When a higher-priority status wins the slot, the pin button is removed from the DOM — this is a deliberate design choice for the merged leading slot (`#150`). The keyboard unpin path is preserved via: (1) focusing the row anchor triggers `group-focus-within` which reveals the dots menu trigger, then Tab → Enter → "Unpin Session"; (2) the context menu (right-click / Shift+F10) exposes "Unpin Session". The "always render + CSS overlay" approach was considered but rejected due to z-index/pointer-events complexity; residual `...` slot behavior is tracked in `#192`. Do NOT flag the absence of the pin button from the DOM when a status is active as an accessibility regression.

Applied to files:

  • packages/opencode/src/session/loop-renderer.ts
📚 Learning: 2026-04-21T16:57:25.580Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 102
File: packages/opencode/src/config/agent.ts:108-119
Timestamp: 2026-04-21T16:57:25.580Z
Learning: In `packages/opencode/src/config/agent.ts` (Astro-Han/pawwork), `ConfigPermission.Info` only accepts permission objects or the three action strings `"ask"`, `"allow"`, `"deny"`, and transforms those action strings into `{ "*": action }` before `normalize()` runs. By the time `normalize()` is reached, `configuredPermission` is always either `undefined` or a `Record<string, Rule>` — never a raw arbitrary string. The `Object.assign(permission, configuredPermission)` pattern is therefore safe. Do not flag it as corrupting string permission references.

Applied to files:

  • packages/opencode/src/session/loop-renderer.ts
📚 Learning: 2026-04-27T08:58:00.665Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 264
File: packages/opencode/src/session/prompt.ts:531-538
Timestamp: 2026-04-27T08:58:00.665Z
Learning: When using Effect (e.g., `yield*` with `Effect`-style generator yielding), only use `yield* new SomeErrorClass(...)` if `SomeErrorClass` extends `Schema.TaggedErrorClass` (i.e., it implements Effect’s Yieldable interface). For plain `Error` subclasses (like `BlockedLoopError` / `LoopStopError`) or inline `new Error(...)` values, they are not yieldable and must be wrapped as `yield* Effect.fail(new PlainError(...))`. Do not recommend changing `yield* Effect.fail(new SomePlainError(...))` to `yield* new SomePlainError(...)` unless the error class extends `Schema.TaggedErrorClass`.

Applied to files:

  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/processor.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Use `testEffect(...)` from `test/lib/effect.ts` for tests that exercise Effect services or Effect-based workflows.

Applied to files:

  • packages/opencode/test/session/prompt-effect.test.ts
📚 Learning: 2026-04-27T08:58:00.665Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 264
File: packages/opencode/src/session/prompt.ts:531-538
Timestamp: 2026-04-27T08:58:00.665Z
Learning: In `packages/opencode/src/session/prompt.ts` (and the broader Astro-Han/pawwork codebase), `yield* new ErrorClass()` is ONLY valid for classes that extend `Schema.TaggedErrorClass` (which implements Effect's Yieldable interface). Plain `Error` subclasses (e.g. `BlockedLoopError`, `LoopStopError` in prompt.ts, or inline `new Error(...)` instances) are NOT yieldable and MUST be wrapped: `yield* Effect.fail(new PlainError(...))`. Do NOT suggest replacing `yield* Effect.fail(new SomePlainError(...))` with `yield* new SomePlainError(...)` unless the class extends `Schema.TaggedErrorClass`.

Applied to files:

  • packages/opencode/test/session/prompt-effect.test.ts
📚 Learning: 2026-04-24T13:03:10.835Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 222
File: packages/desktop-electron/src/main/index.ts:686-692
Timestamp: 2026-04-24T13:03:10.835Z
Learning: In `packages/desktop-electron/src/main/index.ts`, the `checkForUpdates()` function intentionally uses recursive self-invocation for the "Retry" path in the update-check failure dialog. This is mandated by the v5.2 design spec (`#213`): "Await the retry recursion and log any rejection so support can see repeated failures." Because retries are user-paced (require a button click), all prior async frames have already unwound through microtasks before the next attempt, so there is no stack/frame-nesting problem in practice. Do not suggest refactoring this to an iterative loop.

Applied to files:

  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Use `it.effect(...)` when the test should run with `TestClock` and `TestConsole`. Use `it.live(...)` when the test depends on real time, filesystem mtimes, child processes, git, locks, or other live OS behavior.

Applied to files:

  • packages/opencode/test/session/prompt-effect.test.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Avoid custom `ManagedRuntime`, `attach(...)`, or ad hoc `run(...)` wrappers in Effect tests when `testEffect(...)` already provides the runtime.

Applied to files:

  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/src/session/prompt.ts
📚 Learning: 2026-04-25T12:52:36.999Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 234
File: packages/opencode/src/session/export.ts:75-88
Timestamp: 2026-04-25T12:52:36.999Z
Learning: In `packages/opencode/src/session/export.ts` (Astro-Han/pawwork), the `extractReasonFromCause` function intentionally uses a lightweight cast-based inspection of the Cause `reasons` array instead of `Cause.failures()` / `Cause.defects()` utilities. This is a deliberate choice: the function is diagnostic-only (used to populate a best-effort reason string for export diagnostics), and the maintainer explicitly prefers not to add coupling to Effect's Cause API surface for this purpose. Do not flag this pattern as fragile or suggest replacing it with Cause utilities.

Applied to files:

  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-20T14:36:04.113Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/app/e2e/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:04.113Z
Learning: Applies to packages/app/e2e/**/*.spec.ts : Use locator assertions like `toBeVisible()`, `toHaveCount(0)`, and `toHaveAttribute(...)` for normal UI state verification

Applied to files:

  • packages/opencode/test/session/prompt-effect.test.ts
📚 Learning: 2026-04-24T05:48:36.205Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 208
File: packages/app/e2e/app/composer-parity.spec.ts:0-0
Timestamp: 2026-04-24T05:48:36.205Z
Learning: In Astro-Han/pawwork `packages/app/src/components/prompt-input.tsx`, the Model chip trigger button carries `data-action="prompt-model"` (around line 1187) and the Variant chip trigger button carries `data-action="prompt-model-variant"` (around line 1231), both set via `triggerProps`. These are therefore already captured by any `[data-action]` selector sweep in E2E tests and do not need a separate `[data-component]` query to be included in parity assertions — though unioning both is kept as belt-and-suspenders in `collectBarSet`.

Applied to files:

  • packages/opencode/test/session/prompt-effect.test.ts
📚 Learning: 2026-04-21T12:14:30.524Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 93
File: packages/opencode/test/server/cors-middleware.test.ts:15-23
Timestamp: 2026-04-21T12:14:30.524Z
Learning: In `packages/opencode/test/server/cors-middleware.test.ts` (and similar Hono-based test files), `app.request(...)` on the Hono `Hono` app type returns `Promise<Response> | Response` — a union that includes a non-PromiseLike branch. `Effect.promise(...)` requires a `PromiseLike`, so wrapping with `Promise.resolve(app.request(...))` is necessary to normalize the union type. Removing the wrapper causes a TS2322 typecheck error in `packages/opencode`.

Applied to files:

  • packages/opencode/test/session/prompt-effect.test.ts
📚 Learning: 2026-04-23T15:25:31.118Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 193
File: packages/app/e2e/sidebar/sidebar-leading-slot.spec.ts:5-55
Timestamp: 2026-04-23T15:25:31.118Z
Learning: In Astro-Han/pawwork E2E tests, reaching a real "running" session state requires the `project` fixture (for model bootstrap) plus `llm.wait(1)` orchestration. The bare `sdk` fixture used by `withSession` does not trigger an LLM call even with `agent: "build"` + `system` prompt set via `sdk.session.promptAsync`, so simulating running state is not lightweight in the current test infrastructure.

Applied to files:

  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/src/session/processor.ts
  • packages/opencode/test/session/diagnostics.test.ts
📚 Learning: 2026-04-25T12:52:47.074Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 234
File: packages/opencode/src/share/session.ts:27-27
Timestamp: 2026-04-25T12:52:47.074Z
Learning: In `packages/opencode/src/share/session.ts`, the local `ensureEnabled` closure is intentionally duplicated from `ShareRuntime.ensureEnabled` in `runtime.ts`. Using the shared `ShareRuntime.ensureEnabled` effect directly would leak `CloudShareGate` back into the `R` (requirement) type of `share`, `unshare`, and `create`, because the shared effect re-yields the service rather than capturing an already-resolved gate reference. The current pattern resolves `gate` once at the top of the outer `Effect.gen` and then closes over it in a local `ensureEnabled`, keeping the inner effects' requirement types clean. A comment in the file points to `runtime.ts` to make the intentional duplication discoverable. The real fix would be refactoring `ensureEnabled` to accept a gate parameter, but that change is larger than the DRY benefit warrants. Do not flag this duplication as a maintenance issue.

Applied to files:

  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/loop-gate.test.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-25T12:52:32.462Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 234
File: packages/desktop-electron/src/main/ipc.ts:238-263
Timestamp: 2026-04-25T12:52:32.462Z
Learning: In Astro-Han/pawwork (`packages/desktop-electron/src/main/ipc.ts`), `deps.getServerReadyData()` (backed by `serverReady.promise` in `index.ts`) resolves once at server startup and remains settled; it is not expected to reject in practice. Do not flag the absence of a try-catch around it in the `export-session` IPC handler — the network/fetch layer in `server-client.ts` already has a 10-second AbortController timeout and returns a typed `{ok: false, error}` payload, covering the real failure modes.

Applied to files:

  • packages/opencode/src/session/prompt.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/processor.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : In `Effect.gen` / `Effect.fn`, prefer `yield* new MyError(...)` over `yield* Effect.fail(new MyError(...))` for direct early-failure branches

Applied to files:

  • packages/opencode/src/session/prompt.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : Use `Schema.Defect` instead of `unknown` for defect-like causes in Effect code

Applied to files:

  • packages/opencode/src/session/prompt.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : Use `Effect.gen(function* () { ... })` for Effect composition

Applied to files:

  • packages/opencode/src/session/prompt.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : For background loops or scheduled tasks, use `Effect.repeat` or `Effect.schedule` with `Effect.forkScoped` in the layer definition

Applied to files:

  • packages/opencode/src/session/prompt.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : Use `Schema.TaggedErrorClass` for typed errors in Effect schemas

Applied to files:

  • packages/opencode/src/session/prompt.ts
📚 Learning: 2026-04-23T17:02:39.474Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 198
File: packages/ui/src/theme/themes/pawwork.json:32-33
Timestamp: 2026-04-23T17:02:39.474Z
Learning: In Astro-Han/pawwork, `#FF5910` is the locked PawWork brand orange used for `button-primary-base` (and `--accent-brand` in `index.css`, theme token `interactive`/`primary`). The `text-on-interactive-base: `#FFFFFF`` on `button-primary-base: `#FF5910`` combination produces a contrast ratio of ~3.14:1. This intentionally fails WCAG AA normal-text (4.5:1) but passes AA non-text (SC 1.4.11, 3:1) and AA large-text (SC 1.4.3, 3:1). The tradeoff is accepted for brand consistency; do NOT flag this contrast pair as a WCAG violation in future reviews. If a specific surface requires stricter contrast, it should be addressed individually (e.g., larger text, ghost variant, outline button) rather than changing the palette.

Applied to files:

  • packages/opencode/src/session/prompt.ts
📚 Learning: 2026-04-24T06:50:04.597Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 211
File: packages/opencode/src/provider/models.ts:291-304
Timestamp: 2026-04-24T06:50:04.597Z
Learning: In `packages/opencode/src/provider/models.ts`, the `getWithVersion()` function intentionally uses an unbounded `while (true)` retry loop. It only retries when `catalogVersion` changes between the pre- and post-`Data()` read, which is tied to refresh races. The maintainer (Astro-Han) explicitly rejected adding a MAX_RETRIES cap because a hard failure on provider state bootstrap (due to an arbitrary retry limit) is a worse failure mode than retrying until the catalog stabilizes. The loop is expected to self-settle once concurrent writes stop.

Applied to files:

  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-24T06:50:02.712Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 211
File: packages/opencode/src/provider/models.ts:113-179
Timestamp: 2026-04-24T06:50:02.712Z
Learning: In Astro-Han/pawwork (`packages/opencode/src/provider/models.ts`), `PublishModel` and `PublishProvider` intentionally duplicate fields from `Model` and `Provider` with looser optionality. The split is a deliberate behavior boundary: `PublishModel`/`PublishProvider` (with `.passthrough()`) are used for lenient publish-time validation of incoming catalogs, while `Model`/`Provider` are used for strict runtime normalization. Do not flag this duplication as a maintenance burden or suggest extracting a shared base schema — the explicit separation is intentional and tied to the models-refresh reliability path introduced in PR `#211`.

Applied to files:

  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-23T08:51:04.230Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 186
File: packages/opencode/test/plugin/workspace-adaptor.test.ts:139-144
Timestamp: 2026-04-23T08:51:04.230Z
Learning: In Astro-Han/pawwork (`packages/opencode/src/util/filesystem.ts`), the `Filesystem` utility does NOT expose a `remove` or `unlink` helper. The established repository pattern for auth.json teardown in tests (e.g. `provider.test.ts`, `amazon-bedrock.test.ts`, `workspace-adaptor.test.ts`) is to combine `Filesystem.write` with `node:fs/promises unlink`. Do not flag this mixed usage as inconsistent — it is the correct and intentional pattern.

Applied to files:

  • packages/opencode/src/session/diagnostics.ts
🔇 Additional comments (5)
packages/opencode/test/session/compaction.test.ts (1)

159-165: LGTM!

The fake handle correctly implements the new loop-context APIs with deterministic empty/false values appropriate for compaction tests. The Effect.fn usage for the synthetic record methods aligns with coding guidelines.

packages/opencode/test/session/export.test.ts (2)

183-324: LGTM!

The test suite comprehensively covers deriveSnapshotDiagnostics behavior:

  • Block detection with field mapping
  • Empty tree returns {}
  • Timestamp-based selection across child trees
  • Stop overriding earlier block
  • Lone stop without preceding block

The deterministic assistantSeq counter addresses the prior nitpick about Math.random() usage.


326-399: LGTM!

The helper functions are well-structured with clear parameterization for timestamp (end) and identification (tag). The fixture separation between blockToolPartAt and stopToolPartAt makes test intent explicit.

packages/opencode/test/session/loop-gate.test.ts (1)

1-227: LGTM!

Comprehensive test coverage for the loop-gate policy helpers:

  • deriveParentLoopState: validates lastInput/lastError population, completedFailures counting, autoResumeSpent flip, and blockEmitted marking
  • queryGateAction: validates decision outcomes for observe (no failures, < threshold), block (>= 5 with recover emitted), and stop (autoResumeSpent or blockEmitted)

The tests correctly exercise the threshold behavior (5 failures) and signature matching logic documented in the implementation.

packages/opencode/test/session/prompt-effect.test.ts (1)

498-583: LGTM!

The updated test comprehensively validates the loop gate behavior:

  • 7 tool calls to a stable nonexistent path accumulate failures
  • Asserts exactly 1 block and 1 stop synthetic error part
  • Validates metadata: loopType: "target", loopCompletedFailures: 5
  • Validates error text contains "blocked by PawWork" / "halted by PawWork"
  • Validates trailing synthetic text contains "已停止" with no tool parts after stop
  • Validates recovery reminder was fired

The flattened text extraction at lines 564-579 addresses the prior review comment about brittle JSON.stringify assertions by iterating the actual text/input fields from the captured requests.

Comment thread packages/opencode/src/session/diagnostics.ts
Comment thread packages/opencode/src/session/loop-renderer.ts
Comment thread packages/opencode/src/session/processor.ts
Comment thread packages/opencode/src/session/prompt.ts
@Astro-Han Astro-Han force-pushed the claude/issue-229-loop-detection-v1 branch 2 times, most recently from 4e27248 to 5c04848 Compare April 27, 2026 10:58

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/opencode/src/session/processor.ts`:
- Around line 799-806: Replace the unsafe cast of incoming args with the
persisted canonical input from the running part: when calling session.updatePart
to mark the part errored, set state.input to match.part.state.input (the already
persisted, schema-shaped payload) instead of using input.args as Record<string,
any>; apply the same change to the other occurrence that sets state.input (the
similar updateBlock around the later error handling) so you always reuse
match.part.state.input rather than re-casting input.args.

In `@packages/opencode/test/session/diagnostics.test.ts`:
- Around line 289-299: The test title is misleading about cross-tool
accumulation; update the test name for the spec in
SessionDiagnostics.targetSummary to state that it normalizes filePath/filepath
to the same path hash (i.e., verifies normalization only) and mention that
same-target accumulation is tool-scoped (no cross-tool accumulation), so rename
the test string accordingly in the test that currently calls
SessionDiagnostics.targetSummary.

In `@packages/opencode/test/session/export.test.ts`:
- Around line 347-399: The helper functions blockToolPartAt and stopToolPartAt
currently create their own MessageID (messageID: MessageID.make(...)) which
diverges from the enclosing assistant message (info.id from makeAssistantInfo)
and can break ownership lookups like deriveSnapshotDiagnostics that may rely on
part.messageID; change both helpers to accept an assistantMessageID (or
parentMessageID) parameter and use that value for the messageID field instead of
generating a new one so mounted parts share the enclosing assistant's id.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 981e1c9a-b7a8-4079-ba5b-792b7db975e5

📥 Commits

Reviewing files that changed from the base of the PR and between 441e858 and 5c04848.

📒 Files selected for processing (10)
  • packages/opencode/src/session/diagnostics.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/src/session/processor.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: unit-windows-opencode-config-project
  • GitHub Check: unit-windows-app
  • GitHub Check: unit-windows-opencode-server-tools
  • GitHub Check: unit-windows-opencode-session
  • GitHub Check: unit-windows-desktop
  • GitHub Check: unit-opencode
  • GitHub Check: smoke-macos-arm64
  • GitHub Check: e2e-artifacts
  • GitHub Check: analyze-js-ts
🧰 Additional context used
📓 Path-based instructions (2)
packages/opencode/**/*.ts

📄 CodeRabbit inference engine (packages/opencode/AGENTS.md)

packages/opencode/**/*.ts: Use Effect.gen(function* () { ... }) for Effect composition
Use Effect.fn("Domain.method") for named/traced effects and Effect.fnUntraced for internal helpers; these accept pipeable operators as extra arguments to avoid unnecessary outer .pipe() wrappers
Use Effect.callback for callback-based APIs
Prefer DateTime.nowAsDate over new Date(yield* Clock.currentTimeMillis) when you need a Date in Effect code
Use Schema.Class for multi-field data in Effect schemas
Use branded schemas (Schema.brand) for single-value types in Effect
Use Schema.TaggedErrorClass for typed errors in Effect schemas
Use Schema.Defect instead of unknown for defect-like causes in Effect code
In Effect.gen / Effect.fn, prefer yield* new MyError(...) over yield* Effect.fail(new MyError(...)) for direct early-failure branches
Use makeRuntime from src/effect/run-service.ts for all services; it returns { runPromise, runFork, runCallback } backed by a shared memoMap that deduplicates layers
Use InstanceState from src/effect/instance-state.ts for per-directory or per-project state that needs per-instance cleanup; do work directly in the InstanceState.make closure where ScopedCache handles run-once semantics
Use Effect.addFinalizer or Effect.acquireRelease inside the InstanceState.make closure for cleanup (subscriptions, process teardown, etc.)
Use Effect.forkScoped inside the InstanceState.make closure for background stream consumers — the fiber is interrupted when the instance is disposed
Prefer FileSystem.FileSystem instead of raw fs/promises for effectful file I/O in Effect services
Prefer ChildProcessSpawner.ChildProcessSpawner with ChildProcess.make(...) instead of custom process wrappers in Effect services
Prefer HttpClient.HttpClient instead of raw fetch in Effect services
Prefer Path.Path, Config, Clock, and DateTime services when those concerns are already inside Effect code
For backgroun...

Files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/src/session/processor.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/src/session/diagnostics.ts
packages/opencode/test/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (packages/opencode/test/AGENTS.md)

packages/opencode/test/**/*.test.{ts,tsx}: Use the tmpdir function from fixture/fixture.ts to create temporary directories for tests with automatic cleanup. Use await using syntax to ensure automatic cleanup when the variable goes out of scope.
When using the tmpdir function with git repository support, pass the git: true option to initialize a git repo with a root commit.
Use the config option in tmpdir to write an opencode.json config file during test setup by passing a partial Config.Info object.
Use the init option in tmpdir to define custom setup functions that can return extra data accessible via tmp.extra, and use the dispose option for custom cleanup logic.
Use testEffect(...) from test/lib/effect.ts for tests that exercise Effect services or Effect-based workflows.
Use it.effect(...) when the test should run with TestClock and TestConsole. Use it.live(...) when the test depends on real time, filesystem mtimes, child processes, git, locks, or other live OS behavior.
Prefer Effect-aware helpers from fixture/fixture.ts over building manual runtimes in tests: use tmpdirScoped() for scoped temp directories, provideInstance(dir)(effect) for low-level binding without directory creation, provideTmpdirInstance(...) for single temp instance binding, or provideTmpdirServer(...) for tests that also need the test LLM server.
Define const it = testEffect(...) near the top of the test file and keep the test body inside Effect.gen(function* () { ... }). Yield services directly with yield* MyService.Service or yield* MyTool.
Avoid custom ManagedRuntime, attach(...), or ad hoc run(...) wrappers in Effect tests when testEffect(...) already provides the runtime.
When a test needs instance-local state, prefer provideTmpdirInstance(...) or provideInstance(...) over manual Instance.provide(...) inside Promise-style tests.

Files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
🧠 Learnings (46)
📓 Common learnings
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 264
File: packages/opencode/src/session/prompt.ts:108-169
Timestamp: 2026-04-27T10:33:08.974Z
Learning: In Astro-Han/pawwork (`packages/opencode/src/session/prompt.ts` and `packages/opencode/src/session/processor.ts`, PR `#264`), the loop-gate race condition between `buildLoopContext()` and `recordSyntheticBlock`/`recordSyntheticStop` is intentionally handled via idempotence guards (re-check sigKey presence / `hasStopped` inside the record helpers) rather than a full per-parent `Effect.Mutex`. Threading a `Map<MessageID, Mutex>` through the processor was considered too large a surface change for this edge case; the residual TOCTOU window only produces extra synthetic parts with no behavioral drift on the "turn ends" contract. A code comment documents the trade-off and points to a full-mutex follow-up if the race is observed in practice. Do NOT re-flag the absence of a per-parent mutex as a blocking issue in future reviews.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 264
File: packages/opencode/src/session/diagnostics.ts:255-279
Timestamp: 2026-04-27T08:27:40.791Z
Learning: In `packages/opencode/src/session/diagnostics.ts` (PR `#264`, issue `#229`), target-level loop detection is intentionally tool-scoped. The `real` filter includes `r.tool === input.tool` and target signature keys are formatted as `target:${tool}:${targetHash}`. Cross-tool retries on the same URL/path/query are considered legitimate exploration (e.g., webfetch failed → switch to a different tool), NOT a stuck loop. Only same-tool + same-target accumulation counts toward block/stop. Do NOT suggest removing the tool scope from target signature keys or the `real` filter.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 264
File: packages/opencode/src/session/diagnostics.ts:245-252
Timestamp: 2026-04-27T10:32:59.274Z
Learning: In Astro-Han/pawwork (`packages/opencode/src/session/diagnostics.ts`, PR `#264`), `loopLastError` is stored as raw (unscrubbed) error text in loop metadata intentionally. The trust boundary for sensitive data scrubbing is the renderer (`LoopRenderer.render` already applies `scrubErrorText` before any user/model-facing output), not the storage layer. Failed tool parts also already store raw errors in `state.error` (PR `#204` default behavior), so scrubbing only `loopLastError` would not close that pre-existing leak and would add lossy storage. If export-side hardening is needed, the correct approach is a single sanitizer pass over both `state.error` and metadata in the export layer. Do NOT suggest scrubbing `loopLastError` at the `observeToolError` call site.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 264
File: packages/opencode/test/session/diagnostics.test.ts:313-318
Timestamp: 2026-04-27T10:08:52.777Z
Learning: In Astro-Han/pawwork (`packages/opencode/src/session/diagnostics.ts`, PR `#264`), `SessionDiagnostics.LoopAction` is a TypeScript string literal union type (`"observe" | "block" | "stop"`), NOT a runtime enum. There is no runtime form to introspect via `Object.values()`. The test pattern `const all: SessionDiagnostics.LoopAction[] = ["observe", "block", "stop"]; expect(all).toHaveLength(3)` is intentional and correct — it provides compile-time exhaustiveness via the type annotation. Do NOT suggest replacing this with `Object.values()` introspection or flag the runtime length check as insufficient.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 206
File: packages/app/e2e/prompt/prompt-footer-focus.spec.ts:131-143
Timestamp: 2026-04-24T03:51:54.050Z
Learning: In Astro-Han/pawwork E2E tests (packages/app/e2e/fixtures.ts), `project.prompt(text)` internally calls `trackSession(next.sessionID, active.directory)` after the prompt submission is observed and the active session is resolved. Tests that obtain a `sessionID` from `project.prompt()` do NOT need to call `project.trackSession(sessionID)` manually — it is already registered for teardown. Calling it again would be duplicate ownership.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 224
File: packages/app/src/i18n/zh.ts:0-0
Timestamp: 2026-04-24T17:08:44.294Z
Learning: In Astro-Han/pawwork PR `#224`, the first-occurrence `PawWork 爪印` branding rule originally specified in issue `#196` was superseded by an updated Chinese-branding spec. On all zh UI surfaces in `packages/app/src/i18n/zh.ts` (e.g., `dialog.model.unpaid.freeModels.title`, `session.new.subtitle`, `sidebar.gettingStarted.line1`), the correct and intentional target is fully localized `爪印` branding — no `PawWork` prefix. Do NOT flag these strings as missing the first-occurrence `PawWork 爪印` rule in future reviews.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 247
File: packages/ui/src/components/message-part.tsx:1322-1324
Timestamp: 2026-04-26T16:34:54.895Z
Learning: In Astro-Han/pawwork (`packages/ui/src/components/message-part.tsx`), the `taskId` createMemo and `childSessionId` createMemo both intentionally read only from `partMetadata().sessionId` (populated post-execution), not from `input.task_id` / `input.subagent_session_id`. This has always been the case — the original code never read the input field either. Adding an `input.subagent_session_id` fallback would be a new capability, not a bug fix. Do NOT flag the absence of this fallback as a regression in PR `#247` or future PRs unless there is a concrete case where metadata is not populated.
📚 Learning: 2026-04-27T10:33:08.974Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 264
File: packages/opencode/src/session/prompt.ts:108-169
Timestamp: 2026-04-27T10:33:08.974Z
Learning: In Astro-Han/pawwork (`packages/opencode/src/session/prompt.ts` and `packages/opencode/src/session/processor.ts`, PR `#264`), the loop-gate race condition between `buildLoopContext()` and `recordSyntheticBlock`/`recordSyntheticStop` is intentionally handled via idempotence guards (re-check sigKey presence / `hasStopped` inside the record helpers) rather than a full per-parent `Effect.Mutex`. Threading a `Map<MessageID, Mutex>` through the processor was considered too large a surface change for this edge case; the residual TOCTOU window only produces extra synthetic parts with no behavioral drift on the "turn ends" contract. A code comment documents the trade-off and points to a full-mutex follow-up if the race is observed in practice. Do NOT re-flag the absence of a per-parent mutex as a blocking issue in future reviews.

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
📚 Learning: 2026-04-27T11:18:45.896Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 271
File: packages/opencode/test/tool/mcp-exa.test.ts:1-186
Timestamp: 2026-04-27T11:18:45.896Z
Learning: In `packages/opencode/test/tool/mcp-exa.test.ts` (Astro-Han/pawwork), the tests intentionally use raw `bun:test` async cases with `Effect.runPromise(...)` and per-case `HttpClient.make(...)` fakes rather than the `testEffect(...)` harness. The maintainer has explicitly decided not to migrate, because the HttpClient fake wiring is itself the behavior under test and switching to `testEffect` would be style churn without changing risk coverage. Do not flag these tests as needing harness migration.

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/src/session/processor.ts
  • packages/opencode/test/session/diagnostics.test.ts
📚 Learning: 2026-04-20T14:36:04.113Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/app/e2e/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:04.113Z
Learning: Applies to packages/app/e2e/**/*.spec.ts : Use fixture-managed cleanup with `withSession(sdk, title, callback)` for temporary sessions instead of calling `sdk.session.delete(...)` directly

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/export.test.ts
📚 Learning: 2026-04-27T08:27:40.791Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 264
File: packages/opencode/src/session/diagnostics.ts:255-279
Timestamp: 2026-04-27T08:27:40.791Z
Learning: In `packages/opencode/src/session/diagnostics.ts` (PR `#264`, issue `#229`), target-level loop detection is intentionally tool-scoped. The `real` filter includes `r.tool === input.tool` and target signature keys are formatted as `target:${tool}:${targetHash}`. Cross-tool retries on the same URL/path/query are considered legitimate exploration (e.g., webfetch failed → switch to a different tool), NOT a stuck loop. Only same-tool + same-target accumulation counts toward block/stop. Do NOT suggest removing the tool scope from target signature keys or the `real` filter.

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/src/session/processor.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-24T03:51:54.050Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 206
File: packages/app/e2e/prompt/prompt-footer-focus.spec.ts:131-143
Timestamp: 2026-04-24T03:51:54.050Z
Learning: In Astro-Han/pawwork E2E tests (packages/app/e2e/fixtures.ts), `project.prompt(text)` internally calls `trackSession(next.sessionID, active.directory)` after the prompt submission is observed and the active session is resolved. Tests that obtain a `sessionID` from `project.prompt()` do NOT need to call `project.trackSession(sessionID)` manually — it is already registered for teardown. Calling it again would be duplicate ownership.

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/processor.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-27T11:19:21.835Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 271
File: packages/opencode/test/tool/websearch-auth.test.ts:0-0
Timestamp: 2026-04-27T11:19:21.835Z
Learning: In `packages/opencode/test/tool/websearch-auth.test.ts` (Astro-Han/pawwork), the tests intentionally use a small local `runWith` runner with raw `bun:test` and `Effect.runPromise` rather than the `testEffect` harness. Each test case injects a custom in-memory `Auth.Service` layer; switching to `testEffect` would be style-only churn without changing risk coverage. Do not flag these tests as needing harness migration.

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-27T11:18:45.149Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 271
File: packages/opencode/test/agent/agent.test.ts:440-447
Timestamp: 2026-04-27T11:18:45.149Z
Learning: In `packages/opencode/test/agent/agent.test.ts` (Astro-Han/pawwork), all agent-permission tests intentionally use the manual `tmpdir()` + `Instance.provide(...)` pattern. Do not flag individual tests in this file for conversion to `provideTmpdirInstance(...)` or `provideInstance(...)`; a full harness migration would be a separate PR if the pattern ever needs to change.

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-26T16:34:54.895Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 247
File: packages/ui/src/components/message-part.tsx:1322-1324
Timestamp: 2026-04-26T16:34:54.895Z
Learning: In Astro-Han/pawwork (`packages/ui/src/components/message-part.tsx`), the `taskId` createMemo and `childSessionId` createMemo both intentionally read only from `partMetadata().sessionId` (populated post-execution), not from `input.task_id` / `input.subagent_session_id`. This has always been the case — the original code never read the input field either. Adding an `input.subagent_session_id` fallback would be a new capability, not a bug fix. Do NOT flag the absence of this fallback as a regression in PR `#247` or future PRs unless there is a concrete case where metadata is not populated.

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/processor.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Define `const it = testEffect(...)` near the top of the test file and keep the test body inside `Effect.gen(function* () { ... })`. Yield services directly with `yield* MyService.Service` or `yield* MyTool`.

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/src/session/processor.ts
  • packages/opencode/test/session/diagnostics.test.ts
📚 Learning: 2026-04-27T11:18:45.847Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 271
File: packages/opencode/test/tool/websearch.test.ts:21-78
Timestamp: 2026-04-27T11:18:45.847Z
Learning: In `packages/opencode/test/tool/websearch.test.ts`, the tests intentionally use manual `Effect.runPromise` with explicit `Effect.provide(...)` chains (including `Layer.succeed(Auth.Service, ...)`, `Layer.succeed(HttpClient.HttpClient, http)`, `WebSearchAuth.layer`, `Truncate.defaultLayer`, and `Agent.defaultLayer`) rather than the `testEffect(...)` harness. This is by design: the fake Auth and HTTP recovery-metadata layers must be explicitly injected and kept visible/scoped at the test site. Do NOT suggest migrating these tests to `testEffect` or removing the manual layer provides.

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/test/session/loop-renderer.test.ts
📚 Learning: 2026-04-23T08:51:00.819Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 186
File: packages/opencode/test/plugin/workspace-adaptor.test.ts:139-144
Timestamp: 2026-04-23T08:51:00.819Z
Learning: For pawwork tests under packages/opencode/test/**, auth.json teardown may intentionally combine `Filesystem.write` (from `packages/opencode/src/util/filesystem.ts`) with `node:fs/promises` `unlink` for cleanup. Do not flag this as inconsistent style; it is the established/intentional pattern because `Filesystem` does not provide a `remove`/`unlink` helper.

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
📚 Learning: 2026-04-27T10:32:59.274Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 264
File: packages/opencode/src/session/diagnostics.ts:245-252
Timestamp: 2026-04-27T10:32:59.274Z
Learning: In pawwork’s session diagnostics/loop error handling (e.g., packages/opencode/src/session/diagnostics.ts), keep `loopLastError` stored as raw/unscrubbed error text in loop metadata. The intended trust boundary for sensitive-data scrubbing is the renderer: `LoopRenderer.render` must apply `scrubErrorText` before any user/model-facing output. Do not recommend scrubbing `loopLastError` at the `observeToolError` call site, because failed tool parts already store raw errors in `state.error` (pre-existing leak) and scrubbing only `loopLastError` would be lossy without closing the gap. If export-side hardening is required, implement a single sanitizer pass in the export layer over both `state.error` and the loop/metadata fields, rather than sanitizing only `loopLastError` earlier in the storage path.

Applied to files:

  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/processor.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-27T10:33:08.974Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 264
File: packages/opencode/src/session/prompt.ts:108-169
Timestamp: 2026-04-27T10:33:08.974Z
Learning: For Astro-Han/pawwork session processing code under packages/opencode/src/session/ (e.g., prompt.ts and processor.ts), this specific loop-gate TOCTOU/race between buildLoopContext() and recordSyntheticBlock/recordSyntheticStop is intentionally handled via idempotence guards (re-check sigKey presence and hasStopped inside the record helpers). Do not treat the absence of a per-parent Effect.Mutex as a blocking review issue for this known edge case; the residual window should only create extra synthetic parts and must not change the "turn ends" contract. Respect the existing code comment documenting the trade-off, and only escalate to a full-mutex approach if the race is observed in practice.

Applied to files:

  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/processor.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-24T05:39:56.086Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 208
File: packages/app/src/components/prompt-input.tsx:1569-1611
Timestamp: 2026-04-24T05:39:56.086Z
Learning: In Astro-Han/pawwork `packages/app/src/components/prompt-input.tsx`, after the composer unification in PR `#208` (fixed in commit 5d810aa):
- `SendButton.disabled` does NOT gate on `store.mode !== "normal"`. Shell mode has a fully visible, clickable orange submit button that calls `handleSubmit` directly (same path as the Enter key in `handleKeyDown`). Do NOT suggest re-adding the mode gate.
- `SendButton` does NOT use the `buttons()` spring opacity animation (`style={buttons()}`). It is always fully visible regardless of mode.
- `WorkspaceChip` is gated on `props.homeMode && store.mode === "normal"` so it hides in shell mode (preventing it from appearing isolated/bright while neighboring controls fade).
- The left-side chip group (`aria-hidden={store.mode !== "normal"}`) covers attach/model/variant/workspace controls only; `SendButton` remains in a separate right-side sibling div.

Applied to files:

  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/src/session/prompt.ts
📚 Learning: 2026-04-24T17:08:44.294Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 224
File: packages/app/src/i18n/zh.ts:0-0
Timestamp: 2026-04-24T17:08:44.294Z
Learning: In Astro-Han/pawwork PR `#224`, the first-occurrence `PawWork 爪印` branding rule originally specified in issue `#196` was superseded by an updated Chinese-branding spec. On all zh UI surfaces in `packages/app/src/i18n/zh.ts` (e.g., `dialog.model.unpaid.freeModels.title`, `session.new.subtitle`, `sidebar.gettingStarted.line1`), the correct and intentional target is fully localized `爪印` branding — no `PawWork` prefix. Do NOT flag these strings as missing the first-occurrence `PawWork 爪印` rule in future reviews.

Applied to files:

  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/src/session/processor.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-22T09:32:58.310Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 126
File: packages/ui/src/theme/context.tsx:11-16
Timestamp: 2026-04-22T09:32:58.310Z
Learning: In Astro-Han/pawwork (`packages/ui/src/theme/context.tsx` and related files), the renaming of localStorage theme keys from `opencode-*` to `pawwork-*` (THEME_ID, COLOR_SCHEME, THEME_CSS_LIGHT, THEME_CSS_DARK) is intentional and should NOT include a migration path from the old keys. Migrating would re-couple PawWork and OpenCode browser storage namespaces, which the PR is explicitly designed to avoid. A reset to the PawWork default theme on upgrade is acceptable by design.

Applied to files:

  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-25T11:52:40.349Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 233
File: packages/desktop-electron/src/main/server.ts:60-60
Timestamp: 2026-04-25T11:52:40.349Z
Learning: In Astro-Han/pawwork (`packages/desktop-electron/src/main/index.ts`), `ensureLoopbackNoProxy()` (defined at line 551) is called inside `setupApp()` at line 263, which runs synchronously before `initialize()` (line 321) and `spawnLocalServer()` (line 395). It unconditionally upserts `127.0.0.1`, `localhost`, and `::1` into both `process.env.NO_PROXY` and `process.env.no_proxy`. This guarantees that `configureProxyDispatcher(process.env)` in `server.ts` always receives a `process.env` that already excludes loopback from proxying. Do not flag the absence of a loopback-merge inside `configureProxyDispatcher` or `normalizeProxyConfig` — the exclusion is already enforced at a higher layer before those functions are ever called.

Applied to files:

  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-23T15:26:07.250Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 193
File: packages/app/src/pages/layout/sidebar-items.tsx:102-107
Timestamp: 2026-04-23T15:26:07.250Z
Learning: In Astro-Han/pawwork (`packages/app/src/pages/layout/sidebar-items.tsx`), the `indicator()` function in `SessionRow` intentionally renders `props.leadingSlot` (the pin button) only as a fallback when no status indicator (running/permission/error/unseen) is active. When a higher-priority status wins the slot, the pin button is removed from the DOM — this is a deliberate design choice for the merged leading slot (`#150`). The keyboard unpin path is preserved via: (1) focusing the row anchor triggers `group-focus-within` which reveals the dots menu trigger, then Tab → Enter → "Unpin Session"; (2) the context menu (right-click / Shift+F10) exposes "Unpin Session". The "always render + CSS overlay" approach was considered but rejected due to z-index/pointer-events complexity; residual `...` slot behavior is tracked in `#192`. Do NOT flag the absence of the pin button from the DOM when a status is active as an accessibility regression.

Applied to files:

  • packages/opencode/src/session/loop-renderer.ts
📚 Learning: 2026-04-26T15:35:31.757Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 245
File: packages/opencode/src/question/index.ts:21-24
Timestamp: 2026-04-26T15:35:31.757Z
Learning: In Astro-Han/pawwork (`packages/opencode/src/question/index.ts`), `Question.Option.description` is intentionally **required** (`z.string()`, not `.optional()`). This matches the upstream opencode contract and all current callers (e.g. `PlanExitTool`) always supply a description. The defensive `<Show when={props.description}>` rendering in `session-question-dock.tsx` is a standard guard, not a signal that the field is intended to be optional. Do NOT suggest making `Option.description` optional without a dedicated follow-up that covers schema + tool description + dock copy + tests.

Applied to files:

  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/src/session/processor.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-21T16:57:25.580Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 102
File: packages/opencode/src/config/agent.ts:108-119
Timestamp: 2026-04-21T16:57:25.580Z
Learning: In `packages/opencode/src/config/agent.ts` (Astro-Han/pawwork), `ConfigPermission.Info` only accepts permission objects or the three action strings `"ask"`, `"allow"`, `"deny"`, and transforms those action strings into `{ "*": action }` before `normalize()` runs. By the time `normalize()` is reached, `configuredPermission` is always either `undefined` or a `Record<string, Rule>` — never a raw arbitrary string. The `Object.assign(permission, configuredPermission)` pattern is therefore safe. Do not flag it as corrupting string permission references.

Applied to files:

  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-27T08:58:00.665Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 264
File: packages/opencode/src/session/prompt.ts:531-538
Timestamp: 2026-04-27T08:58:00.665Z
Learning: When using Effect (e.g., `yield*` with `Effect`-style generator yielding), only use `yield* new SomeErrorClass(...)` if `SomeErrorClass` extends `Schema.TaggedErrorClass` (i.e., it implements Effect’s Yieldable interface). For plain `Error` subclasses (like `BlockedLoopError` / `LoopStopError`) or inline `new Error(...)` values, they are not yieldable and must be wrapped as `yield* Effect.fail(new PlainError(...))`. Do not recommend changing `yield* Effect.fail(new SomePlainError(...))` to `yield* new SomePlainError(...)` unless the error class extends `Schema.TaggedErrorClass`.

Applied to files:

  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/processor.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-22T08:49:47.800Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 126
File: packages/desktop-electron/src/main/index-sidecar-source.test.ts:3-11
Timestamp: 2026-04-22T08:49:47.800Z
Learning: In `packages/desktop-electron/src/main/index-sidecar-source.test.ts` (Astro-Han/pawwork), the test intentionally uses `expect(source).toContain` / `expect(source).not.toContain` string matching against the raw `index.ts` source text as a lightweight sidecar contract guard. The maintainer has explicitly chosen not to introduce an AST parser (e.g., `babel/parser` or acorn) for this purpose. Do not flag these string-based assertions as fragile or suggest converting them to AST-based matching.

Applied to files:

  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-27T10:08:52.777Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 264
File: packages/opencode/test/session/diagnostics.test.ts:313-318
Timestamp: 2026-04-27T10:08:52.777Z
Learning: In Astro-Han/pawwork (`packages/opencode/src/session/diagnostics.ts`, PR `#264`), `SessionDiagnostics.LoopAction` is a TypeScript string literal union type (`"observe" | "block" | "stop"`), NOT a runtime enum. There is no runtime form to introspect via `Object.values()`. The test pattern `const all: SessionDiagnostics.LoopAction[] = ["observe", "block", "stop"]; expect(all).toHaveLength(3)` is intentional and correct — it provides compile-time exhaustiveness via the type annotation. Do NOT suggest replacing this with `Object.values()` introspection or flag the runtime length check as insufficient.

Applied to files:

  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/processor.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-23T08:51:04.230Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 186
File: packages/opencode/test/plugin/workspace-adaptor.test.ts:139-144
Timestamp: 2026-04-23T08:51:04.230Z
Learning: In Astro-Han/pawwork (`packages/opencode/src/util/filesystem.ts`), the `Filesystem` utility does NOT expose a `remove` or `unlink` helper. The established repository pattern for auth.json teardown in tests (e.g. `provider.test.ts`, `amazon-bedrock.test.ts`, `workspace-adaptor.test.ts`) is to combine `Filesystem.write` with `node:fs/promises unlink`. Do not flag this mixed usage as inconsistent — it is the correct and intentional pattern.

Applied to files:

  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-25T12:52:36.999Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 234
File: packages/opencode/src/session/export.ts:75-88
Timestamp: 2026-04-25T12:52:36.999Z
Learning: In `packages/opencode/src/session/export.ts` (Astro-Han/pawwork), the `extractReasonFromCause` function intentionally uses a lightweight cast-based inspection of the Cause `reasons` array instead of `Cause.failures()` / `Cause.defects()` utilities. This is a deliberate choice: the function is diagnostic-only (used to populate a best-effort reason string for export diagnostics), and the maintainer explicitly prefers not to add coupling to Effect's Cause API surface for this purpose. Do not flag this pattern as fragile or suggest replacing it with Cause utilities.

Applied to files:

  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-27T10:32:59.274Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 264
File: packages/opencode/src/session/diagnostics.ts:245-252
Timestamp: 2026-04-27T10:32:59.274Z
Learning: In Astro-Han/pawwork (`packages/opencode/src/session/diagnostics.ts`, PR `#264`), `loopLastError` is stored as raw (unscrubbed) error text in loop metadata intentionally. The trust boundary for sensitive data scrubbing is the renderer (`LoopRenderer.render` already applies `scrubErrorText` before any user/model-facing output), not the storage layer. Failed tool parts also already store raw errors in `state.error` (PR `#204` default behavior), so scrubbing only `loopLastError` would not close that pre-existing leak and would add lossy storage. If export-side hardening is needed, the correct approach is a single sanitizer pass over both `state.error` and metadata in the export layer. Do NOT suggest scrubbing `loopLastError` at the `observeToolError` call site.

Applied to files:

  • packages/opencode/test/session/export.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Prefer Effect-aware helpers from `fixture/fixture.ts` over building manual runtimes in tests: use `tmpdirScoped()` for scoped temp directories, `provideInstance(dir)(effect)` for low-level binding without directory creation, `provideTmpdirInstance(...)` for single temp instance binding, or `provideTmpdirServer(...)` for tests that also need the test LLM server.

Applied to files:

  • packages/opencode/test/session/export.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
📚 Learning: 2026-04-24T00:02:53.315Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 203
File: packages/app/e2e/sidebar/sidebar-session-links.spec.ts:34-55
Timestamp: 2026-04-24T00:02:53.315Z
Learning: In Astro-Han/pawwork E2E tests (`packages/app/e2e/**/*.spec.ts`), `project.trackDirectory()` and `project.trackSession()` cannot be called before `project.open()` — the `project` fixture throws until `open()` initializes its internal state. The correct pattern is: call `project.trackSession(sessionID)` from inside the `beforeGoto` callback (where state already exists), call `project.trackDirectory(directory)` and cross-workspace `project.trackSession(id, directory)` immediately after `project.open()` returns, and rely on explicit `finally` cleanup (e.g. `cleanupSession` / `cleanupTestProject`) for any resources created before `open()` that cannot yet be tracked via the fixture.

Applied to files:

  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-20T14:36:04.113Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/app/e2e/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:04.113Z
Learning: Applies to packages/app/e2e/**/*.spec.ts : Prefer fluent helpers and drivers when they make intent obvious and reduce locator-heavy noise in tests

Applied to files:

  • packages/opencode/test/session/export.test.ts
📚 Learning: 2026-04-25T12:52:47.074Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 234
File: packages/opencode/src/share/session.ts:27-27
Timestamp: 2026-04-25T12:52:47.074Z
Learning: In `packages/opencode/src/share/session.ts`, the local `ensureEnabled` closure is intentionally duplicated from `ShareRuntime.ensureEnabled` in `runtime.ts`. Using the shared `ShareRuntime.ensureEnabled` effect directly would leak `CloudShareGate` back into the `R` (requirement) type of `share`, `unshare`, and `create`, because the shared effect re-yields the service rather than capturing an already-resolved gate reference. The current pattern resolves `gate` once at the top of the outer `Effect.gen` and then closes over it in a local `ensureEnabled`, keeping the inner effects' requirement types clean. A comment in the file points to `runtime.ts` to make the intentional duplication discoverable. The real fix would be refactoring `ensureEnabled` to accept a gate parameter, but that change is larger than the DRY benefit warrants. Do not flag this duplication as a maintenance issue.

Applied to files:

  • packages/opencode/src/session/prompt.ts
📚 Learning: 2026-04-23T15:25:31.118Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 193
File: packages/app/e2e/sidebar/sidebar-leading-slot.spec.ts:5-55
Timestamp: 2026-04-23T15:25:31.118Z
Learning: In Astro-Han/pawwork E2E tests, reaching a real "running" session state requires the `project` fixture (for model bootstrap) plus `llm.wait(1)` orchestration. The bare `sdk` fixture used by `withSession` does not trigger an LLM call even with `agent: "build"` + `system` prompt set via `sdk.session.promptAsync`, so simulating running state is not lightweight in the current test infrastructure.

Applied to files:

  • packages/opencode/src/session/prompt.ts
  • packages/opencode/src/session/processor.ts
  • packages/opencode/test/session/diagnostics.test.ts
📚 Learning: 2026-04-25T12:52:32.462Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 234
File: packages/desktop-electron/src/main/ipc.ts:238-263
Timestamp: 2026-04-25T12:52:32.462Z
Learning: In Astro-Han/pawwork (`packages/desktop-electron/src/main/ipc.ts`), `deps.getServerReadyData()` (backed by `serverReady.promise` in `index.ts`) resolves once at server startup and remains settled; it is not expected to reject in practice. Do not flag the absence of a try-catch around it in the `export-session` IPC handler — the network/fetch layer in `server-client.ts` already has a 10-second AbortController timeout and returns a typed `{ok: false, error}` payload, covering the real failure modes.

Applied to files:

  • packages/opencode/src/session/prompt.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/processor.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : In `Effect.gen` / `Effect.fn`, prefer `yield* new MyError(...)` over `yield* Effect.fail(new MyError(...))` for direct early-failure branches

Applied to files:

  • packages/opencode/src/session/prompt.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : Use `Schema.Defect` instead of `unknown` for defect-like causes in Effect code

Applied to files:

  • packages/opencode/src/session/prompt.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : Use `Effect.gen(function* () { ... })` for Effect composition

Applied to files:

  • packages/opencode/src/session/prompt.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : For background loops or scheduled tasks, use `Effect.repeat` or `Effect.schedule` with `Effect.forkScoped` in the layer definition

Applied to files:

  • packages/opencode/src/session/prompt.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : Use `Schema.TaggedErrorClass` for typed errors in Effect schemas

Applied to files:

  • packages/opencode/src/session/prompt.ts
📚 Learning: 2026-04-23T17:02:39.474Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 198
File: packages/ui/src/theme/themes/pawwork.json:32-33
Timestamp: 2026-04-23T17:02:39.474Z
Learning: In Astro-Han/pawwork, `#FF5910` is the locked PawWork brand orange used for `button-primary-base` (and `--accent-brand` in `index.css`, theme token `interactive`/`primary`). The `text-on-interactive-base: `#FFFFFF`` on `button-primary-base: `#FF5910`` combination produces a contrast ratio of ~3.14:1. This intentionally fails WCAG AA normal-text (4.5:1) but passes AA non-text (SC 1.4.11, 3:1) and AA large-text (SC 1.4.3, 3:1). The tradeoff is accepted for brand consistency; do NOT flag this contrast pair as a WCAG violation in future reviews. If a specific surface requires stricter contrast, it should be addressed individually (e.g., larger text, ghost variant, outline button) rather than changing the palette.

Applied to files:

  • packages/opencode/src/session/prompt.ts
📚 Learning: 2026-04-21T12:14:30.524Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 93
File: packages/opencode/test/server/cors-middleware.test.ts:15-23
Timestamp: 2026-04-21T12:14:30.524Z
Learning: In `packages/opencode/test/server/cors-middleware.test.ts` (and similar Hono-based test files), `app.request(...)` on the Hono `Hono` app type returns `Promise<Response> | Response` — a union that includes a non-PromiseLike branch. `Effect.promise(...)` requires a `PromiseLike`, so wrapping with `Promise.resolve(app.request(...))` is necessary to normalize the union type. Removing the wrapper causes a TS2322 typecheck error in `packages/opencode`.

Applied to files:

  • packages/opencode/test/session/prompt-effect.test.ts
📚 Learning: 2026-04-24T05:48:36.205Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 208
File: packages/app/e2e/app/composer-parity.spec.ts:0-0
Timestamp: 2026-04-24T05:48:36.205Z
Learning: In Astro-Han/pawwork `packages/app/src/components/prompt-input.tsx`, the Model chip trigger button carries `data-action="prompt-model"` (around line 1187) and the Variant chip trigger button carries `data-action="prompt-model-variant"` (around line 1231), both set via `triggerProps`. These are therefore already captured by any `[data-action]` selector sweep in E2E tests and do not need a separate `[data-component]` query to be included in parity assertions — though unioning both is kept as belt-and-suspenders in `collectBarSet`.

Applied to files:

  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Use `it.effect(...)` when the test should run with `TestClock` and `TestConsole`. Use `it.live(...)` when the test depends on real time, filesystem mtimes, child processes, git, locks, or other live OS behavior.

Applied to files:

  • packages/opencode/test/session/prompt-effect.test.ts
📚 Learning: 2026-04-24T13:03:10.835Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 222
File: packages/desktop-electron/src/main/index.ts:686-692
Timestamp: 2026-04-24T13:03:10.835Z
Learning: In `packages/desktop-electron/src/main/index.ts`, the `checkForUpdates()` function intentionally uses recursive self-invocation for the "Retry" path in the update-check failure dialog. This is mandated by the v5.2 design spec (`#213`): "Await the retry recursion and log any rejection so support can see repeated failures." Because retries are user-paced (require a button click), all prior async frames have already unwound through microtasks before the next attempt, so there is no stack/frame-nesting problem in practice. Do not suggest refactoring this to an iterative loop.

Applied to files:

  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-20T14:36:04.113Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/app/e2e/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:04.113Z
Learning: Applies to packages/app/e2e/packages/app/src/testing/**/*.ts : Test-only hooks must be inert unless explicitly enabled and should not add normal-runtime listeners, reactive subscriptions, or per-update allocations

Applied to files:

  • packages/opencode/test/session/loop-renderer.test.ts
📚 Learning: 2026-04-22T09:32:54.556Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 126
File: packages/opencode/test/provider/provider.test.ts:64-85
Timestamp: 2026-04-22T09:32:54.556Z
Learning: In `packages/opencode/test/provider/provider.test.ts`, the file intentionally uses AppRuntime-based async helpers (`run`, `list`, `getProvider`, etc.) rather than `testEffect(...)` for all tests. Converting individual tests to `testEffect` while leaving the rest on the async pattern would create internal inconsistency. A full harness migration of this file is the right approach if the pattern needs to change, but that should be a separate PR.

Applied to files:

  • packages/opencode/test/session/loop-renderer.test.ts
📚 Learning: 2026-04-24T06:50:04.597Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 211
File: packages/opencode/src/provider/models.ts:291-304
Timestamp: 2026-04-24T06:50:04.597Z
Learning: In `packages/opencode/src/provider/models.ts`, the `getWithVersion()` function intentionally uses an unbounded `while (true)` retry loop. It only retries when `catalogVersion` changes between the pre- and post-`Data()` read, which is tied to refresh races. The maintainer (Astro-Han) explicitly rejected adding a MAX_RETRIES cap because a hard failure on provider state bootstrap (due to an arbitrary retry limit) is a worse failure mode than retrying until the catalog stabilizes. The loop is expected to self-settle once concurrent writes stop.

Applied to files:

  • packages/opencode/src/session/diagnostics.ts
🔇 Additional comments (7)
packages/opencode/test/session/compaction.test.ts (1)

159-164: Fake processor stub stays aligned with the production handle.

The added loop-context members keep this test double compatible with SessionProcessor.Handle without changing compaction behavior.

packages/opencode/test/session/loop-renderer.test.ts (1)

101-216: Strong scrubbing coverage here.

These cases lock in the sensitive path/token/URL redaction behavior that matters most for the new Chinese stop summary.

packages/opencode/src/session/prompt.ts (1)

110-119: Good stop re-entry hardening.

The early hasStopped short-circuit plus StructuredOutput.shouldHalt() closes the auto-resume path that could otherwise execute tools or capture a final answer after the turn was already stopped.

Also applies to: 542-549, 591-598, 1684-1691, 2090-2106

packages/opencode/src/session/processor.ts (1)

286-320: Nice hot-path consolidation here.

buildLoopContext() removes three full parent-message scans from the pre-tool gate and keeps the derived loop inputs in one place.

packages/opencode/src/session/diagnostics.ts (3)

104-131: truncateForRenderer is now resilient to non-serializable payloads.

The try/catch + String(value) fallback closes the circular/BigInt failure path cleanly without regressing the existing UTF-8-safe truncation.


492-517: Legacy reminder fallback preserves migration behavior.

The error: compatibility path avoids silently consuming pending reminders from older sessions.


350-401: No changes required. The syntheticBlockSigKeys passed to deriveParentLoopState() are already properly scoped to the calling parent. The only production call site (prompt.ts:125) obtains these keys from processor.buildLoopContext(parentID), which explicitly filters all messages by parentID before collecting synthetic block signatures (processor.ts:297). There is no risk of cross-parent leakage.

			> Likely an incorrect or invalid review comment.

Comment thread packages/opencode/src/session/processor.ts
Comment thread packages/opencode/test/session/diagnostics.test.ts Outdated
Comment thread packages/opencode/test/session/export.test.ts Outdated
@Astro-Han Astro-Han force-pushed the claude/issue-229-loop-detection-v1 branch from 5c04848 to 413742d Compare April 27, 2026 11:42

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/opencode/test/session/prompt-effect.test.ts`:
- Around line 556-559: Relax the assertion that builds recoverFired: update the
predicate inside errorParts.some(...) (the one that reads
state.metadata?.diagnostics?.loop?.loopRecoverFiredFor) so it accepts either
entries that startWith("input:") OR entries that represent same-target
signatures (e.g., entries without a prefix like no ":" present) instead of only
"input:"-prefixed keys, then keep the expect(recoverFired).toBe(true) check;
modify the anonymous function used to test each k to (k.startsWith("input:") ||
!k.includes(":")) so same-target recovery keys pass too.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: b00a5b93-6c81-4a6a-8fb7-d4554bc74525

📥 Commits

Reviewing files that changed from the base of the PR and between 5c04848 and aeb0178.

📒 Files selected for processing (11)
  • packages/opencode/src/session/diagnostics.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/src/session/processor.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/test/session/loop-gate.test.ts
  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: unit-windows-opencode-config-project
  • GitHub Check: unit-windows-opencode-server-tools
  • GitHub Check: unit-desktop
  • GitHub Check: unit-windows-app
  • GitHub Check: unit-windows-desktop
  • GitHub Check: unit-windows-opencode-session
  • GitHub Check: typecheck
  • GitHub Check: unit-opencode
  • GitHub Check: smoke-macos-arm64
  • GitHub Check: e2e-artifacts
  • GitHub Check: analyze-js-ts
🧰 Additional context used
📓 Path-based instructions (2)
packages/opencode/**/*.ts

📄 CodeRabbit inference engine (packages/opencode/AGENTS.md)

packages/opencode/**/*.ts: Use Effect.gen(function* () { ... }) for Effect composition
Use Effect.fn("Domain.method") for named/traced effects and Effect.fnUntraced for internal helpers; these accept pipeable operators as extra arguments to avoid unnecessary outer .pipe() wrappers
Use Effect.callback for callback-based APIs
Prefer DateTime.nowAsDate over new Date(yield* Clock.currentTimeMillis) when you need a Date in Effect code
Use Schema.Class for multi-field data in Effect schemas
Use branded schemas (Schema.brand) for single-value types in Effect
Use Schema.TaggedErrorClass for typed errors in Effect schemas
Use Schema.Defect instead of unknown for defect-like causes in Effect code
In Effect.gen / Effect.fn, prefer yield* new MyError(...) over yield* Effect.fail(new MyError(...)) for direct early-failure branches
Use makeRuntime from src/effect/run-service.ts for all services; it returns { runPromise, runFork, runCallback } backed by a shared memoMap that deduplicates layers
Use InstanceState from src/effect/instance-state.ts for per-directory or per-project state that needs per-instance cleanup; do work directly in the InstanceState.make closure where ScopedCache handles run-once semantics
Use Effect.addFinalizer or Effect.acquireRelease inside the InstanceState.make closure for cleanup (subscriptions, process teardown, etc.)
Use Effect.forkScoped inside the InstanceState.make closure for background stream consumers — the fiber is interrupted when the instance is disposed
Prefer FileSystem.FileSystem instead of raw fs/promises for effectful file I/O in Effect services
Prefer ChildProcessSpawner.ChildProcessSpawner with ChildProcess.make(...) instead of custom process wrappers in Effect services
Prefer HttpClient.HttpClient instead of raw fetch in Effect services
Prefer Path.Path, Config, Clock, and DateTime services when those concerns are already inside Effect code
For backgroun...

Files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/loop-gate.test.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/src/session/diagnostics.ts
  • packages/opencode/src/session/processor.ts
packages/opencode/test/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (packages/opencode/test/AGENTS.md)

packages/opencode/test/**/*.test.{ts,tsx}: Use the tmpdir function from fixture/fixture.ts to create temporary directories for tests with automatic cleanup. Use await using syntax to ensure automatic cleanup when the variable goes out of scope.
When using the tmpdir function with git repository support, pass the git: true option to initialize a git repo with a root commit.
Use the config option in tmpdir to write an opencode.json config file during test setup by passing a partial Config.Info object.
Use the init option in tmpdir to define custom setup functions that can return extra data accessible via tmp.extra, and use the dispose option for custom cleanup logic.
Use testEffect(...) from test/lib/effect.ts for tests that exercise Effect services or Effect-based workflows.
Use it.effect(...) when the test should run with TestClock and TestConsole. Use it.live(...) when the test depends on real time, filesystem mtimes, child processes, git, locks, or other live OS behavior.
Prefer Effect-aware helpers from fixture/fixture.ts over building manual runtimes in tests: use tmpdirScoped() for scoped temp directories, provideInstance(dir)(effect) for low-level binding without directory creation, provideTmpdirInstance(...) for single temp instance binding, or provideTmpdirServer(...) for tests that also need the test LLM server.
Define const it = testEffect(...) near the top of the test file and keep the test body inside Effect.gen(function* () { ... }). Yield services directly with yield* MyService.Service or yield* MyTool.
Avoid custom ManagedRuntime, attach(...), or ad hoc run(...) wrappers in Effect tests when testEffect(...) already provides the runtime.
When a test needs instance-local state, prefer provideTmpdirInstance(...) or provideInstance(...) over manual Instance.provide(...) inside Promise-style tests.

Files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/loop-gate.test.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/test/session/loop-renderer.test.ts
🧠 Learnings (51)
📓 Common learnings
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 264
File: packages/opencode/src/session/prompt.ts:108-169
Timestamp: 2026-04-27T10:33:08.974Z
Learning: In Astro-Han/pawwork (`packages/opencode/src/session/prompt.ts` and `packages/opencode/src/session/processor.ts`, PR `#264`), the loop-gate race condition between `buildLoopContext()` and `recordSyntheticBlock`/`recordSyntheticStop` is intentionally handled via idempotence guards (re-check sigKey presence / `hasStopped` inside the record helpers) rather than a full per-parent `Effect.Mutex`. Threading a `Map<MessageID, Mutex>` through the processor was considered too large a surface change for this edge case; the residual TOCTOU window only produces extra synthetic parts with no behavioral drift on the "turn ends" contract. A code comment documents the trade-off and points to a full-mutex follow-up if the race is observed in practice. Do NOT re-flag the absence of a per-parent mutex as a blocking issue in future reviews.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 264
File: packages/opencode/src/session/diagnostics.ts:255-279
Timestamp: 2026-04-27T08:27:40.791Z
Learning: In `packages/opencode/src/session/diagnostics.ts` (PR `#264`, issue `#229`), target-level loop detection is intentionally tool-scoped. The `real` filter includes `r.tool === input.tool` and target signature keys are formatted as `target:${tool}:${targetHash}`. Cross-tool retries on the same URL/path/query are considered legitimate exploration (e.g., webfetch failed → switch to a different tool), NOT a stuck loop. Only same-tool + same-target accumulation counts toward block/stop. Do NOT suggest removing the tool scope from target signature keys or the `real` filter.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 264
File: packages/opencode/src/session/diagnostics.ts:245-252
Timestamp: 2026-04-27T10:32:59.274Z
Learning: In Astro-Han/pawwork (`packages/opencode/src/session/diagnostics.ts`, PR `#264`), `loopLastError` is stored as raw (unscrubbed) error text in loop metadata intentionally. The trust boundary for sensitive data scrubbing is the renderer (`LoopRenderer.render` already applies `scrubErrorText` before any user/model-facing output), not the storage layer. Failed tool parts also already store raw errors in `state.error` (PR `#204` default behavior), so scrubbing only `loopLastError` would not close that pre-existing leak and would add lossy storage. If export-side hardening is needed, the correct approach is a single sanitizer pass over both `state.error` and metadata in the export layer. Do NOT suggest scrubbing `loopLastError` at the `observeToolError` call site.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 264
File: packages/opencode/test/session/diagnostics.test.ts:313-318
Timestamp: 2026-04-27T10:08:52.777Z
Learning: In Astro-Han/pawwork (`packages/opencode/src/session/diagnostics.ts`, PR `#264`), `SessionDiagnostics.LoopAction` is a TypeScript string literal union type (`"observe" | "block" | "stop"`), NOT a runtime enum. There is no runtime form to introspect via `Object.values()`. The test pattern `const all: SessionDiagnostics.LoopAction[] = ["observe", "block", "stop"]; expect(all).toHaveLength(3)` is intentional and correct — it provides compile-time exhaustiveness via the type annotation. Do NOT suggest replacing this with `Object.values()` introspection or flag the runtime length check as insufficient.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 206
File: packages/app/e2e/prompt/prompt-footer-focus.spec.ts:131-143
Timestamp: 2026-04-24T03:51:54.050Z
Learning: In Astro-Han/pawwork E2E tests (packages/app/e2e/fixtures.ts), `project.prompt(text)` internally calls `trackSession(next.sessionID, active.directory)` after the prompt submission is observed and the active session is resolved. Tests that obtain a `sessionID` from `project.prompt()` do NOT need to call `project.trackSession(sessionID)` manually — it is already registered for teardown. Calling it again would be duplicate ownership.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 224
File: packages/app/src/i18n/zh.ts:0-0
Timestamp: 2026-04-24T17:08:44.294Z
Learning: In Astro-Han/pawwork PR `#224`, the first-occurrence `PawWork 爪印` branding rule originally specified in issue `#196` was superseded by an updated Chinese-branding spec. On all zh UI surfaces in `packages/app/src/i18n/zh.ts` (e.g., `dialog.model.unpaid.freeModels.title`, `session.new.subtitle`, `sidebar.gettingStarted.line1`), the correct and intentional target is fully localized `爪印` branding — no `PawWork` prefix. Do NOT flag these strings as missing the first-occurrence `PawWork 爪印` rule in future reviews.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 247
File: packages/ui/src/components/message-part.tsx:1322-1324
Timestamp: 2026-04-26T16:34:54.895Z
Learning: In Astro-Han/pawwork (`packages/ui/src/components/message-part.tsx`), the `taskId` createMemo and `childSessionId` createMemo both intentionally read only from `partMetadata().sessionId` (populated post-execution), not from `input.task_id` / `input.subagent_session_id`. This has always been the case — the original code never read the input field either. Adding an `input.subagent_session_id` fallback would be a new capability, not a bug fix. Do NOT flag the absence of this fallback as a regression in PR `#247` or future PRs unless there is a concrete case where metadata is not populated.
📚 Learning: 2026-04-27T10:33:08.974Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 264
File: packages/opencode/src/session/prompt.ts:108-169
Timestamp: 2026-04-27T10:33:08.974Z
Learning: In Astro-Han/pawwork (`packages/opencode/src/session/prompt.ts` and `packages/opencode/src/session/processor.ts`, PR `#264`), the loop-gate race condition between `buildLoopContext()` and `recordSyntheticBlock`/`recordSyntheticStop` is intentionally handled via idempotence guards (re-check sigKey presence / `hasStopped` inside the record helpers) rather than a full per-parent `Effect.Mutex`. Threading a `Map<MessageID, Mutex>` through the processor was considered too large a surface change for this edge case; the residual TOCTOU window only produces extra synthetic parts with no behavioral drift on the "turn ends" contract. A code comment documents the trade-off and points to a full-mutex follow-up if the race is observed in practice. Do NOT re-flag the absence of a per-parent mutex as a blocking issue in future reviews.

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/loop-gate.test.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
📚 Learning: 2026-04-27T11:18:45.896Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 271
File: packages/opencode/test/tool/mcp-exa.test.ts:1-186
Timestamp: 2026-04-27T11:18:45.896Z
Learning: In `packages/opencode/test/tool/mcp-exa.test.ts` (Astro-Han/pawwork), the tests intentionally use raw `bun:test` async cases with `Effect.runPromise(...)` and per-case `HttpClient.make(...)` fakes rather than the `testEffect(...)` harness. The maintainer has explicitly decided not to migrate, because the HttpClient fake wiring is itself the behavior under test and switching to `testEffect` would be style churn without changing risk coverage. Do not flag these tests as needing harness migration.

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/loop-gate.test.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/src/session/processor.ts
📚 Learning: 2026-04-20T14:36:04.113Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/app/e2e/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:04.113Z
Learning: Applies to packages/app/e2e/**/*.spec.ts : Use fixture-managed cleanup with `withSession(sdk, title, callback)` for temporary sessions instead of calling `sdk.session.delete(...)` directly

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/export.test.ts
📚 Learning: 2026-04-27T08:27:40.791Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 264
File: packages/opencode/src/session/diagnostics.ts:255-279
Timestamp: 2026-04-27T08:27:40.791Z
Learning: In `packages/opencode/src/session/diagnostics.ts` (PR `#264`, issue `#229`), target-level loop detection is intentionally tool-scoped. The `real` filter includes `r.tool === input.tool` and target signature keys are formatted as `target:${tool}:${targetHash}`. Cross-tool retries on the same URL/path/query are considered legitimate exploration (e.g., webfetch failed → switch to a different tool), NOT a stuck loop. Only same-tool + same-target accumulation counts toward block/stop. Do NOT suggest removing the tool scope from target signature keys or the `real` filter.

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/loop-gate.test.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/src/session/diagnostics.ts
  • packages/opencode/src/session/processor.ts
📚 Learning: 2026-04-27T11:19:21.835Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 271
File: packages/opencode/test/tool/websearch-auth.test.ts:0-0
Timestamp: 2026-04-27T11:19:21.835Z
Learning: In `packages/opencode/test/tool/websearch-auth.test.ts` (Astro-Han/pawwork), the tests intentionally use a small local `runWith` runner with raw `bun:test` and `Effect.runPromise` rather than the `testEffect` harness. Each test case injects a custom in-memory `Auth.Service` layer; switching to `testEffect` would be style-only churn without changing risk coverage. Do not flag these tests as needing harness migration.

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/loop-gate.test.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/src/session/diagnostics.ts
  • packages/opencode/src/session/processor.ts
📚 Learning: 2026-04-24T03:51:54.050Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 206
File: packages/app/e2e/prompt/prompt-footer-focus.spec.ts:131-143
Timestamp: 2026-04-24T03:51:54.050Z
Learning: In Astro-Han/pawwork E2E tests (packages/app/e2e/fixtures.ts), `project.prompt(text)` internally calls `trackSession(next.sessionID, active.directory)` after the prompt submission is observed and the active session is resolved. Tests that obtain a `sessionID` from `project.prompt()` do NOT need to call `project.trackSession(sessionID)` manually — it is already registered for teardown. Calling it again would be duplicate ownership.

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/loop-gate.test.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/src/session/diagnostics.ts
  • packages/opencode/src/session/processor.ts
📚 Learning: 2026-04-27T11:18:45.149Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 271
File: packages/opencode/test/agent/agent.test.ts:440-447
Timestamp: 2026-04-27T11:18:45.149Z
Learning: In `packages/opencode/test/agent/agent.test.ts` (Astro-Han/pawwork), all agent-permission tests intentionally use the manual `tmpdir()` + `Instance.provide(...)` pattern. Do not flag individual tests in this file for conversion to `provideTmpdirInstance(...)` or `provideInstance(...)`; a full harness migration would be a separate PR if the pattern ever needs to change.

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Define `const it = testEffect(...)` near the top of the test file and keep the test body inside `Effect.gen(function* () { ... })`. Yield services directly with `yield* MyService.Service` or `yield* MyTool`.

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/loop-gate.test.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/test/session/loop-renderer.test.ts
📚 Learning: 2026-04-27T11:18:45.847Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 271
File: packages/opencode/test/tool/websearch.test.ts:21-78
Timestamp: 2026-04-27T11:18:45.847Z
Learning: In `packages/opencode/test/tool/websearch.test.ts`, the tests intentionally use manual `Effect.runPromise` with explicit `Effect.provide(...)` chains (including `Layer.succeed(Auth.Service, ...)`, `Layer.succeed(HttpClient.HttpClient, http)`, `WebSearchAuth.layer`, `Truncate.defaultLayer`, and `Agent.defaultLayer`) rather than the `testEffect(...)` harness. This is by design: the fake Auth and HTTP recovery-metadata layers must be explicitly injected and kept visible/scoped at the test site. Do NOT suggest migrating these tests to `testEffect` or removing the manual layer provides.

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/test/session/loop-renderer.test.ts
📚 Learning: 2026-04-23T08:51:00.819Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 186
File: packages/opencode/test/plugin/workspace-adaptor.test.ts:139-144
Timestamp: 2026-04-23T08:51:00.819Z
Learning: For pawwork tests under packages/opencode/test/**, auth.json teardown may intentionally combine `Filesystem.write` (from `packages/opencode/src/util/filesystem.ts`) with `node:fs/promises` `unlink` for cleanup. Do not flag this as inconsistent style; it is the established/intentional pattern because `Filesystem` does not provide a `remove`/`unlink` helper.

Applied to files:

  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/loop-gate.test.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/test/session/loop-renderer.test.ts
📚 Learning: 2026-04-27T10:08:52.777Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 264
File: packages/opencode/test/session/diagnostics.test.ts:313-318
Timestamp: 2026-04-27T10:08:52.777Z
Learning: In Astro-Han/pawwork (`packages/opencode/src/session/diagnostics.ts`, PR `#264`), `SessionDiagnostics.LoopAction` is a TypeScript string literal union type (`"observe" | "block" | "stop"`), NOT a runtime enum. There is no runtime form to introspect via `Object.values()`. The test pattern `const all: SessionDiagnostics.LoopAction[] = ["observe", "block", "stop"]; expect(all).toHaveLength(3)` is intentional and correct — it provides compile-time exhaustiveness via the type annotation. Do NOT suggest replacing this with `Object.values()` introspection or flag the runtime length check as insufficient.

Applied to files:

  • packages/opencode/test/session/loop-gate.test.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/src/session/diagnostics.ts
  • packages/opencode/src/session/processor.ts
📚 Learning: 2026-04-25T12:52:47.074Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 234
File: packages/opencode/src/share/session.ts:27-27
Timestamp: 2026-04-25T12:52:47.074Z
Learning: In `packages/opencode/src/share/session.ts`, the local `ensureEnabled` closure is intentionally duplicated from `ShareRuntime.ensureEnabled` in `runtime.ts`. Using the shared `ShareRuntime.ensureEnabled` effect directly would leak `CloudShareGate` back into the `R` (requirement) type of `share`, `unshare`, and `create`, because the shared effect re-yields the service rather than capturing an already-resolved gate reference. The current pattern resolves `gate` once at the top of the outer `Effect.gen` and then closes over it in a local `ensureEnabled`, keeping the inner effects' requirement types clean. A comment in the file points to `runtime.ts` to make the intentional duplication discoverable. The real fix would be refactoring `ensureEnabled` to accept a gate parameter, but that change is larger than the DRY benefit warrants. Do not flag this duplication as a maintenance issue.

Applied to files:

  • packages/opencode/test/session/loop-gate.test.ts
  • packages/opencode/src/session/prompt.ts
📚 Learning: 2026-04-27T10:32:59.274Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 264
File: packages/opencode/src/session/diagnostics.ts:245-252
Timestamp: 2026-04-27T10:32:59.274Z
Learning: In Astro-Han/pawwork (`packages/opencode/src/session/diagnostics.ts`, PR `#264`), `loopLastError` is stored as raw (unscrubbed) error text in loop metadata intentionally. The trust boundary for sensitive data scrubbing is the renderer (`LoopRenderer.render` already applies `scrubErrorText` before any user/model-facing output), not the storage layer. Failed tool parts also already store raw errors in `state.error` (PR `#204` default behavior), so scrubbing only `loopLastError` would not close that pre-existing leak and would add lossy storage. If export-side hardening is needed, the correct approach is a single sanitizer pass over both `state.error` and metadata in the export layer. Do NOT suggest scrubbing `loopLastError` at the `observeToolError` call site.

Applied to files:

  • packages/opencode/test/session/loop-gate.test.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/test/session/loop-renderer.test.ts
📚 Learning: 2026-04-20T14:36:04.113Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/app/e2e/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:04.113Z
Learning: Applies to packages/app/e2e/packages/app/src/testing/**/*.ts : Test-only hooks must be inert unless explicitly enabled and should not add normal-runtime listeners, reactive subscriptions, or per-update allocations

Applied to files:

  • packages/opencode/test/session/loop-gate.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/test/session/loop-renderer.test.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Prefer Effect-aware helpers from `fixture/fixture.ts` over building manual runtimes in tests: use `tmpdirScoped()` for scoped temp directories, `provideInstance(dir)(effect)` for low-level binding without directory creation, `provideTmpdirInstance(...)` for single temp instance binding, or `provideTmpdirServer(...)` for tests that also need the test LLM server.

Applied to files:

  • packages/opencode/test/session/export.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/test/session/loop-renderer.test.ts
📚 Learning: 2026-04-22T08:49:47.800Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 126
File: packages/desktop-electron/src/main/index-sidecar-source.test.ts:3-11
Timestamp: 2026-04-22T08:49:47.800Z
Learning: In `packages/desktop-electron/src/main/index-sidecar-source.test.ts` (Astro-Han/pawwork), the test intentionally uses `expect(source).toContain` / `expect(source).not.toContain` string matching against the raw `index.ts` source text as a lightweight sidecar contract guard. The maintainer has explicitly chosen not to introduce an AST parser (e.g., `babel/parser` or acorn) for this purpose. Do not flag these string-based assertions as fragile or suggest converting them to AST-based matching.

Applied to files:

  • packages/opencode/test/session/export.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/src/session/diagnostics.ts
  • packages/opencode/src/session/processor.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Use the `tmpdir` function from `fixture/fixture.ts` to create temporary directories for tests with automatic cleanup. Use `await using` syntax to ensure automatic cleanup when the variable goes out of scope.

Applied to files:

  • packages/opencode/test/session/export.test.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : When a test needs instance-local state, prefer `provideTmpdirInstance(...)` or `provideInstance(...)` over manual `Instance.provide(...)` inside Promise-style tests.

Applied to files:

  • packages/opencode/test/session/export.test.ts
📚 Learning: 2026-04-26T16:34:54.895Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 247
File: packages/ui/src/components/message-part.tsx:1322-1324
Timestamp: 2026-04-26T16:34:54.895Z
Learning: In Astro-Han/pawwork (`packages/ui/src/components/message-part.tsx`), the `taskId` createMemo and `childSessionId` createMemo both intentionally read only from `partMetadata().sessionId` (populated post-execution), not from `input.task_id` / `input.subagent_session_id`. This has always been the case — the original code never read the input field either. Adding an `input.subagent_session_id` fallback would be a new capability, not a bug fix. Do NOT flag the absence of this fallback as a regression in PR `#247` or future PRs unless there is a concrete case where metadata is not populated.

Applied to files:

  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/src/session/diagnostics.ts
  • packages/opencode/src/session/processor.ts
📚 Learning: 2026-04-24T17:08:44.294Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 224
File: packages/app/src/i18n/zh.ts:0-0
Timestamp: 2026-04-24T17:08:44.294Z
Learning: In Astro-Han/pawwork PR `#224`, the first-occurrence `PawWork 爪印` branding rule originally specified in issue `#196` was superseded by an updated Chinese-branding spec. On all zh UI surfaces in `packages/app/src/i18n/zh.ts` (e.g., `dialog.model.unpaid.freeModels.title`, `session.new.subtitle`, `sidebar.gettingStarted.line1`), the correct and intentional target is fully localized `爪印` branding — no `PawWork` prefix. Do NOT flag these strings as missing the first-occurrence `PawWork 爪印` rule in future reviews.

Applied to files:

  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/src/session/diagnostics.ts
  • packages/opencode/src/session/processor.ts
📚 Learning: 2026-04-25T11:52:40.349Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 233
File: packages/desktop-electron/src/main/server.ts:60-60
Timestamp: 2026-04-25T11:52:40.349Z
Learning: In Astro-Han/pawwork (`packages/desktop-electron/src/main/index.ts`), `ensureLoopbackNoProxy()` (defined at line 551) is called inside `setupApp()` at line 263, which runs synchronously before `initialize()` (line 321) and `spawnLocalServer()` (line 395). It unconditionally upserts `127.0.0.1`, `localhost`, and `::1` into both `process.env.NO_PROXY` and `process.env.no_proxy`. This guarantees that `configureProxyDispatcher(process.env)` in `server.ts` always receives a `process.env` that already excludes loopback from proxying. Do not flag the absence of a loopback-merge inside `configureProxyDispatcher` or `normalizeProxyConfig` — the exclusion is already enforced at a higher layer before those functions are ever called.

Applied to files:

  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-26T15:35:31.757Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 245
File: packages/opencode/src/question/index.ts:21-24
Timestamp: 2026-04-26T15:35:31.757Z
Learning: In Astro-Han/pawwork (`packages/opencode/src/question/index.ts`), `Question.Option.description` is intentionally **required** (`z.string()`, not `.optional()`). This matches the upstream opencode contract and all current callers (e.g. `PlanExitTool`) always supply a description. The defensive `<Show when={props.description}>` rendering in `session-question-dock.tsx` is a standard guard, not a signal that the field is intended to be optional. Do NOT suggest making `Option.description` optional without a dedicated follow-up that covers schema + tool description + dock copy + tests.

Applied to files:

  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/src/session/diagnostics.ts
  • packages/opencode/src/session/processor.ts
📚 Learning: 2026-04-20T14:36:04.113Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/app/e2e/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:04.113Z
Learning: Applies to packages/app/e2e/**/*.spec.ts : Prefer fluent helpers and drivers when they make intent obvious and reduce locator-heavy noise in tests

Applied to files:

  • packages/opencode/test/session/export.test.ts
📚 Learning: 2026-04-25T12:52:36.999Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 234
File: packages/opencode/src/session/export.ts:75-88
Timestamp: 2026-04-25T12:52:36.999Z
Learning: In `packages/opencode/src/session/export.ts` (Astro-Han/pawwork), the `extractReasonFromCause` function intentionally uses a lightweight cast-based inspection of the Cause `reasons` array instead of `Cause.failures()` / `Cause.defects()` utilities. This is a deliberate choice: the function is diagnostic-only (used to populate a best-effort reason string for export diagnostics), and the maintainer explicitly prefers not to add coupling to Effect's Cause API surface for this purpose. Do not flag this pattern as fragile or suggest replacing it with Cause utilities.

Applied to files:

  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/src/session/diagnostics.ts
  • packages/opencode/src/session/processor.ts
📚 Learning: 2026-04-27T10:33:08.974Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 264
File: packages/opencode/src/session/prompt.ts:108-169
Timestamp: 2026-04-27T10:33:08.974Z
Learning: For Astro-Han/pawwork session processing code under packages/opencode/src/session/ (e.g., prompt.ts and processor.ts), this specific loop-gate TOCTOU/race between buildLoopContext() and recordSyntheticBlock/recordSyntheticStop is intentionally handled via idempotence guards (re-check sigKey presence and hasStopped inside the record helpers). Do not treat the absence of a per-parent Effect.Mutex as a blocking review issue for this known edge case; the residual window should only create extra synthetic parts and must not change the "turn ends" contract. Respect the existing code comment documenting the trade-off, and only escalate to a full-mutex approach if the race is observed in practice.

Applied to files:

  • packages/opencode/src/session/prompt.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/src/session/diagnostics.ts
  • packages/opencode/src/session/processor.ts
📚 Learning: 2026-04-27T08:58:00.665Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 264
File: packages/opencode/src/session/prompt.ts:531-538
Timestamp: 2026-04-27T08:58:00.665Z
Learning: When using Effect (e.g., `yield*` with `Effect`-style generator yielding), only use `yield* new SomeErrorClass(...)` if `SomeErrorClass` extends `Schema.TaggedErrorClass` (i.e., it implements Effect’s Yieldable interface). For plain `Error` subclasses (like `BlockedLoopError` / `LoopStopError`) or inline `new Error(...)` values, they are not yieldable and must be wrapped as `yield* Effect.fail(new PlainError(...))`. Do not recommend changing `yield* Effect.fail(new SomePlainError(...))` to `yield* new SomePlainError(...)` unless the error class extends `Schema.TaggedErrorClass`.

Applied to files:

  • packages/opencode/src/session/prompt.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/src/session/diagnostics.ts
  • packages/opencode/src/session/processor.ts
📚 Learning: 2026-04-27T10:32:59.274Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 264
File: packages/opencode/src/session/diagnostics.ts:245-252
Timestamp: 2026-04-27T10:32:59.274Z
Learning: In pawwork’s session diagnostics/loop error handling (e.g., packages/opencode/src/session/diagnostics.ts), keep `loopLastError` stored as raw/unscrubbed error text in loop metadata. The intended trust boundary for sensitive-data scrubbing is the renderer: `LoopRenderer.render` must apply `scrubErrorText` before any user/model-facing output. Do not recommend scrubbing `loopLastError` at the `observeToolError` call site, because failed tool parts already store raw errors in `state.error` (pre-existing leak) and scrubbing only `loopLastError` would be lossy without closing the gap. If export-side hardening is required, implement a single sanitizer pass in the export layer over both `state.error` and the loop/metadata fields, rather than sanitizing only `loopLastError` earlier in the storage path.

Applied to files:

  • packages/opencode/src/session/prompt.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/src/session/diagnostics.ts
  • packages/opencode/src/session/processor.ts
📚 Learning: 2026-04-23T15:25:31.118Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 193
File: packages/app/e2e/sidebar/sidebar-leading-slot.spec.ts:5-55
Timestamp: 2026-04-23T15:25:31.118Z
Learning: In Astro-Han/pawwork E2E tests, reaching a real "running" session state requires the `project` fixture (for model bootstrap) plus `llm.wait(1)` orchestration. The bare `sdk` fixture used by `withSession` does not trigger an LLM call even with `agent: "build"` + `system` prompt set via `sdk.session.promptAsync`, so simulating running state is not lightweight in the current test infrastructure.

Applied to files:

  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/src/session/processor.ts
📚 Learning: 2026-04-25T12:52:32.462Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 234
File: packages/desktop-electron/src/main/ipc.ts:238-263
Timestamp: 2026-04-25T12:52:32.462Z
Learning: In Astro-Han/pawwork (`packages/desktop-electron/src/main/ipc.ts`), `deps.getServerReadyData()` (backed by `serverReady.promise` in `index.ts`) resolves once at server startup and remains settled; it is not expected to reject in practice. Do not flag the absence of a try-catch around it in the `export-session` IPC handler — the network/fetch layer in `server-client.ts` already has a 10-second AbortController timeout and returns a typed `{ok: false, error}` payload, covering the real failure modes.

Applied to files:

  • packages/opencode/src/session/prompt.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/diagnostics.ts
  • packages/opencode/src/session/processor.ts
📚 Learning: 2026-04-24T00:02:53.315Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 203
File: packages/app/e2e/sidebar/sidebar-session-links.spec.ts:34-55
Timestamp: 2026-04-24T00:02:53.315Z
Learning: In Astro-Han/pawwork E2E tests (`packages/app/e2e/**/*.spec.ts`), `project.trackDirectory()` and `project.trackSession()` cannot be called before `project.open()` — the `project` fixture throws until `open()` initializes its internal state. The correct pattern is: call `project.trackSession(sessionID)` from inside the `beforeGoto` callback (where state already exists), call `project.trackDirectory(directory)` and cross-workspace `project.trackSession(id, directory)` immediately after `project.open()` returns, and rely on explicit `finally` cleanup (e.g. `cleanupSession` / `cleanupTestProject`) for any resources created before `open()` that cannot yet be tracked via the fixture.

Applied to files:

  • packages/opencode/src/session/prompt.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : In `Effect.gen` / `Effect.fn`, prefer `yield* new MyError(...)` over `yield* Effect.fail(new MyError(...))` for direct early-failure branches

Applied to files:

  • packages/opencode/src/session/prompt.ts
  • packages/opencode/src/session/processor.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : Use `Schema.Defect` instead of `unknown` for defect-like causes in Effect code

Applied to files:

  • packages/opencode/src/session/prompt.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : Use `Effect.gen(function* () { ... })` for Effect composition

Applied to files:

  • packages/opencode/src/session/prompt.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : For background loops or scheduled tasks, use `Effect.repeat` or `Effect.schedule` with `Effect.forkScoped` in the layer definition

Applied to files:

  • packages/opencode/src/session/prompt.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : Use `Schema.TaggedErrorClass` for typed errors in Effect schemas

Applied to files:

  • packages/opencode/src/session/prompt.ts
📚 Learning: 2026-04-22T09:32:58.310Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 126
File: packages/ui/src/theme/context.tsx:11-16
Timestamp: 2026-04-22T09:32:58.310Z
Learning: In Astro-Han/pawwork (`packages/ui/src/theme/context.tsx` and related files), the renaming of localStorage theme keys from `opencode-*` to `pawwork-*` (THEME_ID, COLOR_SCHEME, THEME_CSS_LIGHT, THEME_CSS_DARK) is intentional and should NOT include a migration path from the old keys. Migrating would re-couple PawWork and OpenCode browser storage namespaces, which the PR is explicitly designed to avoid. A reset to the PawWork default theme on upgrade is acceptable by design.

Applied to files:

  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-23T17:02:39.474Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 198
File: packages/ui/src/theme/themes/pawwork.json:32-33
Timestamp: 2026-04-23T17:02:39.474Z
Learning: In Astro-Han/pawwork, `#FF5910` is the locked PawWork brand orange used for `button-primary-base` (and `--accent-brand` in `index.css`, theme token `interactive`/`primary`). The `text-on-interactive-base: `#FFFFFF`` on `button-primary-base: `#FF5910`` combination produces a contrast ratio of ~3.14:1. This intentionally fails WCAG AA normal-text (4.5:1) but passes AA non-text (SC 1.4.11, 3:1) and AA large-text (SC 1.4.3, 3:1). The tradeoff is accepted for brand consistency; do NOT flag this contrast pair as a WCAG violation in future reviews. If a specific surface requires stricter contrast, it should be addressed individually (e.g., larger text, ghost variant, outline button) rather than changing the palette.

Applied to files:

  • packages/opencode/src/session/prompt.ts
📚 Learning: 2026-04-24T05:39:56.086Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 208
File: packages/app/src/components/prompt-input.tsx:1569-1611
Timestamp: 2026-04-24T05:39:56.086Z
Learning: In Astro-Han/pawwork `packages/app/src/components/prompt-input.tsx`, after the composer unification in PR `#208` (fixed in commit 5d810aa):
- `SendButton.disabled` does NOT gate on `store.mode !== "normal"`. Shell mode has a fully visible, clickable orange submit button that calls `handleSubmit` directly (same path as the Enter key in `handleKeyDown`). Do NOT suggest re-adding the mode gate.
- `SendButton` does NOT use the `buttons()` spring opacity animation (`style={buttons()}`). It is always fully visible regardless of mode.
- `WorkspaceChip` is gated on `props.homeMode && store.mode === "normal"` so it hides in shell mode (preventing it from appearing isolated/bright while neighboring controls fade).
- The left-side chip group (`aria-hidden={store.mode !== "normal"}`) covers attach/model/variant/workspace controls only; `SendButton` remains in a separate right-side sibling div.

Applied to files:

  • packages/opencode/src/session/prompt.ts
  • packages/opencode/src/session/loop-renderer.ts
📚 Learning: 2026-04-24T13:03:10.835Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 222
File: packages/desktop-electron/src/main/index.ts:686-692
Timestamp: 2026-04-24T13:03:10.835Z
Learning: In `packages/desktop-electron/src/main/index.ts`, the `checkForUpdates()` function intentionally uses recursive self-invocation for the "Retry" path in the update-check failure dialog. This is mandated by the v5.2 design spec (`#213`): "Await the retry recursion and log any rejection so support can see repeated failures." Because retries are user-paced (require a button click), all prior async frames have already unwound through microtasks before the next attempt, so there is no stack/frame-nesting problem in practice. Do not suggest refactoring this to an iterative loop.

Applied to files:

  • packages/opencode/src/session/export.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Use `testEffect(...)` from `test/lib/effect.ts` for tests that exercise Effect services or Effect-based workflows.

Applied to files:

  • packages/opencode/test/session/prompt-effect.test.ts
📚 Learning: 2026-04-21T12:14:30.524Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 93
File: packages/opencode/test/server/cors-middleware.test.ts:15-23
Timestamp: 2026-04-21T12:14:30.524Z
Learning: In `packages/opencode/test/server/cors-middleware.test.ts` (and similar Hono-based test files), `app.request(...)` on the Hono `Hono` app type returns `Promise<Response> | Response` — a union that includes a non-PromiseLike branch. `Effect.promise(...)` requires a `PromiseLike`, so wrapping with `Promise.resolve(app.request(...))` is necessary to normalize the union type. Removing the wrapper causes a TS2322 typecheck error in `packages/opencode`.

Applied to files:

  • packages/opencode/test/session/prompt-effect.test.ts
📚 Learning: 2026-04-24T05:48:36.205Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 208
File: packages/app/e2e/app/composer-parity.spec.ts:0-0
Timestamp: 2026-04-24T05:48:36.205Z
Learning: In Astro-Han/pawwork `packages/app/src/components/prompt-input.tsx`, the Model chip trigger button carries `data-action="prompt-model"` (around line 1187) and the Variant chip trigger button carries `data-action="prompt-model-variant"` (around line 1231), both set via `triggerProps`. These are therefore already captured by any `[data-action]` selector sweep in E2E tests and do not need a separate `[data-component]` query to be included in parity assertions — though unioning both is kept as belt-and-suspenders in `collectBarSet`.

Applied to files:

  • packages/opencode/test/session/prompt-effect.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Use `it.effect(...)` when the test should run with `TestClock` and `TestConsole`. Use `it.live(...)` when the test depends on real time, filesystem mtimes, child processes, git, locks, or other live OS behavior.

Applied to files:

  • packages/opencode/test/session/prompt-effect.test.ts
📚 Learning: 2026-04-23T15:26:07.250Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 193
File: packages/app/src/pages/layout/sidebar-items.tsx:102-107
Timestamp: 2026-04-23T15:26:07.250Z
Learning: In Astro-Han/pawwork (`packages/app/src/pages/layout/sidebar-items.tsx`), the `indicator()` function in `SessionRow` intentionally renders `props.leadingSlot` (the pin button) only as a fallback when no status indicator (running/permission/error/unseen) is active. When a higher-priority status wins the slot, the pin button is removed from the DOM — this is a deliberate design choice for the merged leading slot (`#150`). The keyboard unpin path is preserved via: (1) focusing the row anchor triggers `group-focus-within` which reveals the dots menu trigger, then Tab → Enter → "Unpin Session"; (2) the context menu (right-click / Shift+F10) exposes "Unpin Session". The "always render + CSS overlay" approach was considered but rejected due to z-index/pointer-events complexity; residual `...` slot behavior is tracked in `#192`. Do NOT flag the absence of the pin button from the DOM when a status is active as an accessibility regression.

Applied to files:

  • packages/opencode/src/session/loop-renderer.ts
📚 Learning: 2026-04-21T16:57:25.580Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 102
File: packages/opencode/src/config/agent.ts:108-119
Timestamp: 2026-04-21T16:57:25.580Z
Learning: In `packages/opencode/src/config/agent.ts` (Astro-Han/pawwork), `ConfigPermission.Info` only accepts permission objects or the three action strings `"ask"`, `"allow"`, `"deny"`, and transforms those action strings into `{ "*": action }` before `normalize()` runs. By the time `normalize()` is reached, `configuredPermission` is always either `undefined` or a `Record<string, Rule>` — never a raw arbitrary string. The `Object.assign(permission, configuredPermission)` pattern is therefore safe. Do not flag it as corrupting string permission references.

Applied to files:

  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/src/session/diagnostics.ts
  • packages/opencode/src/session/processor.ts
📚 Learning: 2026-04-23T08:51:04.230Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 186
File: packages/opencode/test/plugin/workspace-adaptor.test.ts:139-144
Timestamp: 2026-04-23T08:51:04.230Z
Learning: In Astro-Han/pawwork (`packages/opencode/src/util/filesystem.ts`), the `Filesystem` utility does NOT expose a `remove` or `unlink` helper. The established repository pattern for auth.json teardown in tests (e.g. `provider.test.ts`, `amazon-bedrock.test.ts`, `workspace-adaptor.test.ts`) is to combine `Filesystem.write` with `node:fs/promises unlink`. Do not flag this mixed usage as inconsistent — it is the correct and intentional pattern.

Applied to files:

  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-22T09:32:54.556Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 126
File: packages/opencode/test/provider/provider.test.ts:64-85
Timestamp: 2026-04-22T09:32:54.556Z
Learning: In `packages/opencode/test/provider/provider.test.ts`, the file intentionally uses AppRuntime-based async helpers (`run`, `list`, `getProvider`, etc.) rather than `testEffect(...)` for all tests. Converting individual tests to `testEffect` while leaving the rest on the async pattern would create internal inconsistency. A full harness migration of this file is the right approach if the pattern needs to change, but that should be a separate PR.

Applied to files:

  • packages/opencode/test/session/loop-renderer.test.ts
📚 Learning: 2026-04-21T13:45:45.149Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 99
File: packages/desktop-electron/src/renderer/i18n/index.ts:30-35
Timestamp: 2026-04-21T13:45:45.149Z
Learning: In Astro-Han/pawwork, the locale normalization helpers in `packages/app/src/context/language.tsx` (`normalizeLocale`) and `packages/desktop-electron/src/renderer/i18n/index.ts` (`parseLocale`) are intentionally kept separate and have different fallback shapes: `normalizeLocale` always returns a concrete `Locale` (falling back to `"en"`), while `parseLocale` returns `Locale | null` so the desktop shell can decide whether to fall back to browser detection. Do not suggest extracting a shared normalization helper across these two runtimes.

Applied to files:

  • packages/opencode/test/session/loop-renderer.test.ts
📚 Learning: 2026-04-24T06:50:04.597Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 211
File: packages/opencode/src/provider/models.ts:291-304
Timestamp: 2026-04-24T06:50:04.597Z
Learning: In `packages/opencode/src/provider/models.ts`, the `getWithVersion()` function intentionally uses an unbounded `while (true)` retry loop. It only retries when `catalogVersion` changes between the pre- and post-`Data()` read, which is tied to refresh races. The maintainer (Astro-Han) explicitly rejected adding a MAX_RETRIES cap because a hard failure on provider state bootstrap (due to an arbitrary retry limit) is a worse failure mode than retrying until the catalog stabilizes. The loop is expected to self-settle once concurrent writes stop.

Applied to files:

  • packages/opencode/src/session/diagnostics.ts
📚 Learning: 2026-04-24T06:50:02.712Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 211
File: packages/opencode/src/provider/models.ts:113-179
Timestamp: 2026-04-24T06:50:02.712Z
Learning: In Astro-Han/pawwork (`packages/opencode/src/provider/models.ts`), `PublishModel` and `PublishProvider` intentionally duplicate fields from `Model` and `Provider` with looser optionality. The split is a deliberate behavior boundary: `PublishModel`/`PublishProvider` (with `.passthrough()`) are used for lenient publish-time validation of incoming catalogs, while `Model`/`Provider` are used for strict runtime normalization. Do not flag this duplication as a maintenance burden or suggest extracting a shared base schema — the explicit separation is intentional and tied to the models-refresh reliability path introduced in PR `#211`.

Applied to files:

  • packages/opencode/src/session/processor.ts

Comment thread packages/opencode/test/session/prompt-effect.test.ts
@Astro-Han Astro-Han force-pushed the claude/issue-229-loop-detection-v1 branch from aeb0178 to 345aa8d Compare April 27, 2026 12:12
@Astro-Han

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@Astro-Han Astro-Han force-pushed the claude/issue-229-loop-detection-v1 branch from 345aa8d to 6d360aa Compare April 27, 2026 12:40

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/opencode/src/session/loop-renderer.ts`:
- Around line 107-116: scrubErrorText currently only replaces drive-letter and
absolute slash paths, letting relative paths like "../Secrets/token.txt" or
"./config/dev.json" through; update the scrubErrorText function to add replace
passes that detect and replace relative paths (patterns beginning with "./" and
"../") with "<path>" (add after the drive-letter/forward-slash replacements and
before the generic leading-slash regex) so occurrences like "./foo/bar" and
"../foo/bar" are scrubbed; reference the scrubErrorText function to locate where
to insert the additional .replace(...) calls and ensure they handle both single-
and double-dot relative segments and stop at common delimiters (quotes,
whitespace, comma, semicolon, parentheses, brackets, newline).

In `@packages/opencode/src/session/processor.ts`:
- Around line 775-781: When the duplicate guards (the
syntheticBlockSigKeys(parentID) check and the auto-resume duplicate check around
the settleToolCall(...) calls) short-circuit, the in-memory part is removed but
never transitioned to a terminal persisted state; before yielding into
settleToolCall(input.toolCallId) you must write a terminal state for the current
tool part (the part referenced by input.toolCallId / ctx.toolcalls entry) into
persistence and remove it from ctx.toolcalls so the session cannot be left with
a forever-running/pending part. Locate the two guard sites that call
settleToolCall (the branch using syntheticBlockSigKeys(parentID) and the other
auto-resume duplicate branch) and add a short sequence that marks the current
part terminal (using the existing persistence/part-update helper in this file or
codebase) and clears ctx.toolcalls[input.toolCallId] prior to calling yield*
settleToolCall(input.toolCallId).

In `@packages/opencode/test/session/loop-gate.test.ts`:
- Around line 36-47: The test uses two identical failingErrorRecord entries so
it can't detect whether deriveParentLoopState
(SessionDiagnostics.deriveParentLoopState) picks the latest record; update the
second record in the records array returned by failingErrorRecord to have a
distinct lastError (and ideally a different lastInput/url) so the assertions on
SignatureState.lastInput and lastError for sigKey
(`target:webfetch:${targetHashFor(url)}`) will only pass if
deriveParentLoopState uses the latest record; adjust the test setup to create
two non-identical records (e.g., different error string and/or input) and assert
the state reflects the second one.

In `@packages/opencode/test/session/prompt-effect.test.ts`:
- Around line 550-556: The test only checks the first trailing text part after
the synthetic stop; instead assert that among trailing parts there is exactly
one text part and that that single text part is the synthetic stop summary.
Locate the variables stopIdx, allParts and trailing in prompt-effect.test.ts,
replace the loose check using trailing.find(...) with an assertion that
trailing.filter(p => p.type === "text") has length 1 and that its sole element
has synthetic === true and its text contains "stopped"; keep the existing
assertion that trailing.filter(p => p.type === "tool") has length 0.
- Around line 568-587: The test currently asserts that flattenedText contains
the exact phrase "repeated the same tool input 3 times" (via the expect on
flattenedText derived from llm.inputs), which rejects valid reminders phrased
for the target signature; change the assertion to accept either the same-input
variant or the target-level variant by checking flattenedText.some against both
candidates (e.g., accept strings matching /repeated the same tool input 3 times/
OR a target-signature phrasing such as /repeated the same target input 3 times/
or use a more general regex that matches "repeated", "same", and "3 times"),
updating the expect call that references flattenedText so the test passes for
either valid reminder wording.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 7e41fbd7-1f32-48db-95a9-ba636f670ceb

📥 Commits

Reviewing files that changed from the base of the PR and between 345aa8d and 6d360aa.

📒 Files selected for processing (11)
  • packages/opencode/src/session/diagnostics.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/loop-renderer.ts
  • packages/opencode/src/session/processor.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/test/session/loop-gate.test.ts
  • packages/opencode/test/session/loop-renderer.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts

Comment thread packages/opencode/src/session/loop-renderer.ts
Comment thread packages/opencode/src/session/processor.ts
Comment thread packages/opencode/test/session/loop-gate.test.ts
Comment thread packages/opencode/test/session/prompt-effect.test.ts
Comment thread packages/opencode/test/session/prompt-effect.test.ts Outdated
@Astro-Han Astro-Han force-pushed the claude/issue-229-loop-detection-v1 branch from 6d360aa to a3a84f4 Compare April 27, 2026 12:58
@Astro-Han Astro-Han merged commit dc67400 into dev Apr 27, 2026
23 checks passed
@Astro-Han Astro-Han deleted the claude/issue-229-loop-detection-v1 branch April 27, 2026 13:12
@coderabbitai coderabbitai Bot mentioned this pull request May 29, 2026
13 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request harness Model harness, prompts, tool descriptions, and session mechanics P1 High priority

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] Detect low-yield repeated probing on the same investigation target

1 participant