Repository navigation
fix: enforce bundled agent and Loop ownership - #326
Conversation
Enable the bundled dev-cycle extension on fresh homes while preserving explicit state. Keep Loop workers under one executor, fail closed when required hosted tools are unavailable, reconcile cancellations durably, and replace stopped-session ledger projections across resume.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughThe PR adds deferred Loop cancellation reconciliation across storage, services, scheduling, and daemon boot. It also updates session ledger recovery, hosted MCP validation, Loop-worker guards, bundled extension installation, approval metadata, bridge state handling, event payload handling, prompt fixtures, database opening, and task-list polling. ChangesLoop cancellation reconciliation
Session recovery and hosted MCP validation
Loop-worker and extension runtime guards
Runtime and persistence consistency
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant LoopService
participant GlobalDB
participant Scheduler
participant Daemon
participant Session
LoopService->>GlobalDB: persist deferred cancellation
Scheduler->>Daemon: run cancellation backstop
Daemon->>GlobalDB: list pending cancellations
Daemon->>LoopService: reconcile pending cancellations
LoopService->>Session: retry cancellation delivery
LoopService-->>Scheduler: reconciled count and errors
Possibly related PRs
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
React Doctor found no new issues. 🎉 Reviewed by React Doctor for commit |
|
| Filename | Overview |
|---|---|
| internal/loop/service_cancellation_reconcile.go | Adds durable, retryable reconciliation for requested and delivering Loop cancellations. |
| internal/store/globaldb/global_db_loop_cancel_pending.go | Adds persisted discovery and hydration of pending cancellation deliveries. |
| internal/session/manager_resume.go | Discards stopped ledger projections before resume and restores them if provider startup fails. |
| internal/session/ledger.go | Adds ledger discard and rematerialization operations supporting resumed-session continuity. |
| internal/session/manager_start_mcp.go | Enforces hosted-MCP availability before starting sessions whose effective policy requires tools. |
| internal/daemon/task_role_runtime.go | Fences Loop-owned action workers from ordinary task-role activation. |
| internal/extension/install_managed.go | Applies generic disabled defaults before caller options so bundled installation can opt into fresh-home enablement. |
| web/src/systems/tasks/lib/query-options.ts | Removes interval refetching from the task catalog while preserving explicit refresh behavior. |
Sequence Diagram
sequenceDiagram
participant Operator
participant LoopService as Loop service
participant Store as Global store
participant Reconciler as Boot/Scheduler reconciler
participant Session as Loop-owned session
Operator->>LoopService: Request cooperative cancellation
LoopService->>Store: Persist requested state
LoopService->>Session: Attempt cancellation delivery
alt Delivery acknowledged
LoopService->>Store: Advance to draining
else Delivery unavailable or fails
Reconciler->>Store: Read requested/delivering cancellations
Reconciler->>Session: Retry cancellation delivery
Reconciler->>Store: Advance to draining
end
Reviews (5): Last reviewed commit: "fix: stabilize terminal session reads" | Re-trigger Greptile
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (2)
internal/scheduler/scheduler_test.go (1)
1025-1054: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd coverage for a failing cancellation backstop.
fakeLoopCancellationTaskSource.ReconcilePendingCancellationsalways returns a nil error.RunOncewraps a backstop error as"scheduler: Loop cancellation backstop: %w"and continues the rest of the cycle instead of aborting it. That resilience behavior is untested.Add a subtest where the fake returns a partial count and an error. Assert that
RunOncereturns the wrapped error withErrorContains, thatresult.ReconciledCancellationskeeps the partial count, and that the cycle still loads snapshots.As per path instructions: "Focus on critical paths: workflow execution, state management, error handling".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/scheduler/scheduler_test.go` around lines 1025 - 1054, Add a subtest alongside TestRunOnceRunsLoopCancellationBackstop that configures fakeLoopCancellationTaskSource.ReconcilePendingCancellations to return a partial reconciled count and an error. Assert RunOnce returns an error containing “scheduler: Loop cancellation backstop:”, preserves the partial count in result.ReconciledCancellations, and still verifies the cycle loads snapshots after the backstop failure.Source: Path instructions
internal/daemon/loop_cancellation_reconcile.go (1)
29-57: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low valueAdd a comment for the fail-open reconciliation path.
state.loggeris assigned inbootConfigbeforebootComponentsruns, so the dereference after the missing-capability and actor-derivation checks is safe. The reconciliation failure path still defers to the scheduler retry; add a short comment to avoid future callers changing it to fail boot.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/daemon/loop_cancellation_reconcile.go` around lines 29 - 57, Add a brief comment in reconcileBootLoopCancellations before or around the ReconcilePendingCancellations error handling documenting that reconciliation is intentionally fail-open and failures defer to the scheduler retry rather than failing boot. Keep the existing state.logger usage and return behavior unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@internal/daemon/scheduler_runtime.go`:
- Around line 63-68: Ensure the failure branch in the Loops
cancellation-reconciler type assertion shuts down the already-created and
configured scheduler session waker before returning the unsupported-service
error. Reuse the same cleanup behavior as later scheduler construction failures,
while leaving the successful reconciliation path unchanged.
In `@internal/daemon/task_role_runtime.go`:
- Around line 269-271: Update starvationActivation to immediately return the
inactive result when run.IsLoopWorker() is true, matching the existing guard in
the related activation path. Ensure loop-worker runs cannot proceed to
startStarvationSession or create a normal task-role session, while preserving
existing behavior for non-loop workers.
In `@internal/session/ledger_test.go`:
- Around line 208-213: Strengthen the rematerialized-ledger assertions in the
failed-resume test by reading the ledger content after the existence check and
verifying it contains the original prompt and an EventTypeSessionStopped event.
Keep the existing stopped metadata assertion, but ensure the test validates
restored history rather than file presence alone.
In `@internal/store/globaldb/global_db_loop_cancel_pending.go`:
- Around line 66-78: Update the run-level query in the pending-cancellation flow
to derive cancel_reason, cancel_actor_kind, and cancel_actor_id from one
correlated control row rather than applying independent MIN() aggregates. Select
the control with the earliest non-null cancel_requested_at while preserving the
existing run grouping and cancellation-state behavior, so
PendingCancellation.RequestedBy and its reason remain from the same cancellation
event.
In `@internal/store/globaldb/global_db_loop_test.go`:
- Around line 246-251: Strengthen the cross-workspace fixture in the relevant
global DB loop test by requesting cancellation for foreignRun after creating it,
so it satisfies the pending-cancellation filter. Update the exact
workspace-owned run delivery assertions to verify the command for run.ID
contains only session-cancel-owned and the command for foreignRun.ID contains
only session-cancel-other, ensuring workspace isolation is actually tested.
---
Nitpick comments:
In `@internal/daemon/loop_cancellation_reconcile.go`:
- Around line 29-57: Add a brief comment in reconcileBootLoopCancellations
before or around the ReconcilePendingCancellations error handling documenting
that reconciliation is intentionally fail-open and failures defer to the
scheduler retry rather than failing boot. Keep the existing state.logger usage
and return behavior unchanged.
In `@internal/scheduler/scheduler_test.go`:
- Around line 1025-1054: Add a subtest alongside
TestRunOnceRunsLoopCancellationBackstop that configures
fakeLoopCancellationTaskSource.ReconcilePendingCancellations to return a partial
reconciled count and an error. Assert RunOnce returns an error containing
“scheduler: Loop cancellation backstop:”, preserves the partial count in
result.ReconciledCancellations, and still verifies the cycle loads snapshots
after the backstop failure.
🪄 Autofix
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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: f86f773f-ab68-4697-8173-f80b52aee3e8
⛔ Files ignored due to path filters (22)
docs/_memory/glossary.mdis excluded by!**/*.mddocs/qa/charters/CH-loop-worker-exclusive-dispatch.mdis excluded by!**/*.mddocs/qa/reports/2026-08-06-loop-agent-ownership.mdis excluded by!**/*.mddocs/qa/scenarios/ET-052.mdis excluded by!**/*.mddocs/qa/scenarios/ET-compozy-native-tool-invocation.mdis excluded by!**/*.mddocs/qa/scenarios/ET-managed-session-skill-loading.mdis excluded by!**/*.mddocs/qa/scenarios/LP-016.mdis excluded by!**/*.mddocs/qa/scenarios/LP-cancel-vs-kill.mdis excluded by!**/*.mddocs/qa/scenarios/RT-018.mdis excluded by!**/*.mddocs/qa/scenarios/TA-002.mdis excluded by!**/*.mddocs/qa/scenarios/TA-loop-worker-exclusive-dispatch.mdis excluded by!**/*.mddocs/qa/scenarios/TA-task-role-session-activation.mdis excluded by!**/*.mdpackages/site/content/docs/agents/definitions.mdxis excluded by!**/*.mdxpackages/site/content/docs/autonomy/task-runs-and-leases.mdxis excluded by!**/*.mdxpackages/site/content/docs/configuration/agent-md.mdxis excluded by!**/*.mdxpackages/site/content/docs/configuration/file-locations.mdxis excluded by!**/*.mdxpackages/site/content/docs/loops/catalog.mdxis excluded by!**/*.mdxpackages/site/content/docs/loops/index.mdxis excluded by!**/*.mdxpackages/site/content/docs/loops/running.mdxis excluded by!**/*.mdxpackages/site/content/docs/memory/dream.mdxis excluded by!**/*.mdxpackages/site/content/docs/sessions/resume.mdxis excluded by!**/*.mdxpackages/site/content/docs/tools/index.mdxis excluded by!**/*.mdx
📒 Files selected for processing (41)
extensions/dev-cycle/embed_test.goextensions/dev-cycle/install.gointernal/coordinator/coordinator.gointernal/coordinator/coordinator_test.gointernal/daemon/boot_components.gointernal/daemon/coordinator_runtime_test.gointernal/daemon/daemon_test.gointernal/daemon/loop_cancellation_reconcile.gointernal/daemon/scheduler_loop_cancellation.gointernal/daemon/scheduler_runtime.gointernal/daemon/scheduler_task_source_types.gointernal/daemon/task_role_runtime.gointernal/daemon/task_role_runtime_test.gointernal/extension/install_managed.gointernal/extension/install_managed_test.gointernal/loop/cancel_control.gointernal/loop/service_cancellation.gointernal/loop/service_cancellation_reconcile.gointernal/loop/service_test.gointernal/loop/start_binding.gointernal/loop/watch_events_ledger.gointernal/scheduler/scheduler.gointernal/scheduler/scheduler_test.gointernal/scheduler/types.gointernal/session/ledger.gointernal/session/ledger_test.gointernal/session/manager.gointernal/session/manager_clear_family_cleanup.gointernal/session/manager_clear_test.gointernal/session/manager_lineage_test.gointernal/session/manager_prompt_contract_test.gointernal/session/manager_resume.gointernal/session/manager_start_mcp.gointernal/session/manager_test.gointernal/session/manager_test_support_test.gointernal/session/soul_test.gointernal/session/spawn_test.gointernal/store/globaldb/global_db_loop_cancel_pending.gointernal/store/globaldb/global_db_loop_test.gointernal/store/globaldb/global_db_task_test.goweb/src/systems/tasks/lib/query-options.ts
💤 Files with no reviewable changes (1)
- web/src/systems/tasks/lib/query-options.ts
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@internal/daemon/task_role_sessions.go`:
- Around line 181-183: Move the run.IsLoopWorker() early return in
activateForStarvation to immediately after the existing context validation and
before resolveStarvationAgent, while retaining the later guard as defense in
depth. Add a test using a failing or counting resolver to verify Loop-worker
runs skip agent resolution and return without an error.
In `@internal/scheduler/scheduler_test.go`:
- Around line 1082-1086: Update the RunOnce assertion in the scheduler test to
check err is non-nil before calling errors.Is or err.Error(). Keep the wrapped
backstopErr and message validations after that guard so an unexpected nil error
produces a normal test failure instead of a panic.
🪄 Autofix
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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: a1fc5f39-3278-4850-96e2-891e3e4f0eb2
📒 Files selected for processing (8)
internal/daemon/loop_cancellation_reconcile.gointernal/daemon/scheduler_runtime.gointernal/daemon/task_role_runtime_test.gointernal/daemon/task_role_sessions.gointernal/scheduler/scheduler_test.gointernal/session/ledger_test.gointernal/store/globaldb/global_db_loop_cancel_pending.gointernal/store/globaldb/global_db_loop_test.go
🚧 Files skipped from review as they are similar to previous changes (5)
- internal/session/ledger_test.go
- internal/daemon/loop_cancellation_reconcile.go
- internal/daemon/scheduler_runtime.go
- internal/store/globaldb/global_db_loop_cancel_pending.go
- internal/store/globaldb/global_db_loop_test.go
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
internal/testutil/acpmock/fixture_test.go (1)
990-999: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winCover the contract-preservation branches.
This case covers only successful stripping.
stripTrailingLoopOutputContractalso preserves prompts when the marker is absent or the suffix is not a JSON object. Add table cases for malformed JSON, a valid non-object value, and marker-like user text if that input is supported.As per path instructions, tests must “Ensure tests verify behavior outcomes, not just function calls.”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/testutil/acpmock/fixture_test.go` around lines 990 - 999, Add table-driven cases for stripTrailingLoopOutputContract covering malformed JSON after the marker, a valid JSON value that is not an object, and marker-like user text when supported. Assert each result preserves the original prompt, while retaining the existing successful-stripping case and verifying observable output rather than merely invoking the function.Source: Path instructions
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@internal/daemon/tool_approval_bridge.go`:
- Around line 123-132: Update toolApprovalCallID and the approval request
construction so calls without a reliable call identity receive a newly
generated, per-call approval request ID rather than reusable metadata values.
Use that generated ID consistently for both PermissionRequestIDMetaKey and
ToolCallId, and add coverage proving repeated fallback approvals within the same
session receive distinct IDs and cannot affect each other.
---
Nitpick comments:
In `@internal/testutil/acpmock/fixture_test.go`:
- Around line 990-999: Add table-driven cases for
stripTrailingLoopOutputContract covering malformed JSON after the marker, a
valid JSON value that is not an object, and marker-like user text when
supported. Assert each result preserves the original prompt, while retaining the
existing successful-stripping case and verifying observable output rather than
merely invoking the function.
🪄 Autofix
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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 26a2d755-8b0a-434b-a820-ab571c33a3df
⛔ Files ignored due to path filters (3)
web/e2e/__tests__/marketplace.spec.tsis excluded by!web/e2e/**web/e2e/__tests__/settings.spec.tsis excluded by!web/e2e/**web/e2e/__tests__/triggers-hardening.spec.tsis excluded by!web/e2e/**
📒 Files selected for processing (15)
internal/acp/permission.gointernal/acp/permission_helpers.gointernal/bridges/resource_projection.gointernal/daemon/tool_approval_bridge.gointernal/daemon/tool_approval_bridge_test.gointernal/daemon/tool_approval_grants_integration_test.gointernal/session/manager_prompt_contract_test.gointernal/session/manager_prompt_pump.gointernal/session/manager_prompt_runtime_loop.gointernal/store/globaldb/global_db_bridge.gointernal/store/globaldb/global_db_bridges_test.gointernal/store/globaldb/queries/bridge_core.sqlinternal/store/globaldb/sqlcgen/bridge_core.sql.gointernal/testutil/acpmock/fixture_prompt_strip.gointernal/testutil/acpmock/fixture_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
- internal/session/manager_prompt_contract_test.go
| requestID := toolApprovalCallID(call, view) | ||
|
|
||
| response, err := sessions.RequestPermission( | ||
| approvalCtx, | ||
| sessionID, | ||
| acp.RequestPermissionRequest{ | ||
| Meta: map[string]any{acp.PermissionRequestIDMetaKey: requestID}, | ||
| SessionId: acpsdk.SessionId(sessionID), | ||
| ToolCall: acpsdk.ToolCallUpdate{ | ||
| ToolCallId: acpsdk.ToolCallId(toolApprovalCallID(call, view)), | ||
| ToolCallId: acpsdk.ToolCallId(requestID), |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n -C 6 'toolApprovalCallID|RequestToolApproval|ToolCallID|CorrelationID' --glob '*.go' . || true
rg -n -C 8 '\bCallRequest\s*\{|\bToolCallID\s*:' --glob '*.go' . || trueRepository: compozy/compozy
Length of output: 50372
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '--- locating tool_approval_bridge.go ---\n'
fd -a 'tool_approval_bridge\.go$' . || true
printf '\n--- relevant file outline ---\n'
ast-grep outline internal/daemon/tool_approval_bridge.go || true
printf '\n--- relevant file lines ---\n'
sed -n '1,260p' internal/daemon/tool_approval_bridge.go
printf '\n--- focused definitions and approvals ---\n'
rg -n -C 4 'func .*RequestToolApproval|func .*ApplyToolApproval|func .*toolApprovalCallID|type CallRequest|ToolCallID|CorrelationID|ToolCall update|Approval.*Pending|pending request|decisions|decision' internal/daemon internal/tools internal -g '*.go' | sed -n '1,240p'Repository: compozy/compozy
Length of output: 244
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- locating tool_approval_bridge.go ---'
fd -a 'tool_approval_bridge\.go$' . || true
echo
echo '--- relevant file outline ---'
ast-grep outline internal/daemon/tool_approval_bridge.go || true
echo
echo '--- relevant file lines ---'
sed -n '1,260p' internal/daemon/tool_approval_bridge.go
echo
echo '--- focused definitions and approvals ---'
rg -n -C 4 'func .*RequestToolApproval|func .*ApplyToolApproval|func .*toolApprovalCallID|type CallRequest|ToolCallID|CorrelationID|ToolCall update|Approval.*Pending|pending request|decisions|decision' internal/daemon internal/tools internal -g '*.go' | sed -n '1,260p'Repository: compozy/compozy
Length of output: 27757
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- CallRequest definition/usages ---'
rg -n -C 5 'type CallRequest|ToolCallID\s+|CorrelationID\s+|ApprovalToken' internal -g '*.go' | sed -n '1,320p'
echo
echo '--- approval bridge tests mentioning rejection/fallback ---'
fd -a 'native_tool_approval*|tool_approval_bridge*' internal | sed -n '1,120p'
if [ -f internal/daemon/native_tool_approval_test.go ]; then
sed -n '1,280p' internal/daemon/native_tool_approval_test.go
else
echo 'internal/daemon/native_tool_approval_test.go not found'
fi
echo
echo '--- approval contracts definitions ---'
rg -n -C 8 'type ApprovalBridge|Method RequestToolApproval|ApplyToolApproval|ToolCallID|CorrelationID' internal/tools internal -g '*.go' | sed -n '1,260p'Repository: compozy/compozy
Length of output: 41631
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- tool_approval_bridge_test.go ---'
sed -n '1,420p' internal/daemon/tool_approval_bridge_test.go
echo
echo '--- approval bridge implementation and bridge invocation ---'
sed -n '260,460p' internal/tools/tool.go
sed -n '130,240p' internal/tools/dispatch.go
rg -n -C 5 'RequestToolApproval|approve-all|ApprovalAvailable|permission|ApprovalRequired|ApprovalUnreachable|ApprovalTimedOut|ApprovalCanceled' internal/daemon -g '*.go' | sed -n '1,320p'Repository: compozy/compozy
Length of output: 49667
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- approval bridge tests continuation ---'
sed -n '420,660p' internal/daemon/tool_approval_bridge_test.go
echo
echo '--- CallRequest identity fields in non-test production tools internal ---'
python3 - <<'PY'
from pathlib import Path
import re
needle = re.compile(r'CallRequest\{')
for p in sorted(Path('internal').rglob('*.go')):
if '/testdata/' in str(p) or p.suffix != '.go':
continue
text = p.read_text()
if text.startswith('package ') and (text.startswith('package test') or text.startswith('package mocks')):
continue
ms = [m for m in needle.finditer(text) if not text[m.start()-200:m.start()].strip().startswith('//') and not text[m.start()-200:m.start()].strip().startswith('/*')]
if not ms:
continue
print(f'FILE {p}')
for m in ms:
line = text.count('\n', 0, m.start()) + 1
print(f' line {line}')
block = text[m.start():]
brace = 0
for i, ch in enumerate(block):
if ch == '{':
brace += 1
elif ch == '}':
brace -= 1
if brace == 0:
body = block[:i+1]
break
else:
body = block
for field in ['ToolCallID', 'CorrelationID', 'SessionID', 'ApprovalToken', 'ToolID']:
hits = [(x.strip()+':', f'{field}:') for x in body.split('\n') if (field+':') in x or field + ',' in x]
print(f' {field}:', ' '.join(hits[:5]))
echo
echo '--- production calls to approval bridge ---'
rg -n -C 8 'bridge\.RequestToolApproval|RequestToolApproval\(' internal -g '*.go' --glob '!*_test.go' | sed -n '1,260p'Repository: compozy/compozy
Length of output: 8078
🏁 Script executed:
#!/bin/bash
set -euo pipefail
export LC_ALL=C
echo '--- approved-tool-call callers (non-test) ---'
rg -n -C 8 'ApprovedToolCall|approved-tool-call|approve-tool-call|ApproveToolCall|ApprovalRequired' internal -g '*.go' --glob '!*_test.go' | sed -n '1,260p'
echo
echo '--- production Read/Approval bridge wiring references'
rg -n -C 5 'approvalbridge|ApprovalBridge|ApprovedToolCall|toolApprovals|ApproveToolCall|ToolApprovalsSet|ApprovalRequired|approvalprobe' internal -g '*.go' --glob '!*_test.go' | sed -n '1,320p'
echo
echo '--- tool_approval_bridge_grants.go relevant sections'
sed -n '1,320p' internal/daemon/tool_approval_bridge_grants.goRepository: compozy/compozy
Length of output: 44077
Generate a per-call approval request ID when no call identity is available.
toolApprovalCallID falls back to non-unique metadata values, and that value is sent as both request_id and ToolCallId. A second approval from the same caller can map to the first pending request, so the rejection path and “allow once” grants can apply to the wrong call. Use a generated approval request ID for these fallbacks, and cover same-session reuse in the fallback case.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@internal/daemon/tool_approval_bridge.go` around lines 123 - 132, Update
toolApprovalCallID and the approval request construction so calls without a
reliable call identity receive a newly generated, per-call approval request ID
rather than reusable metadata values. Use that generated ID consistently for
both PermissionRequestIDMetaKey and ToolCallId, and add coverage proving
repeated fallback approvals within the same session receive distinct IDs and
cannot affect each other.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@internal/store/sessiondb/read_only_pool.go`:
- Around line 213-215: Update the opening flow around p.open and
context.WithoutCancel so the detached opener always has an explicit bounded
timeout, including NewReadOnlyPool’s default OpenSessionDBReadOnly path.
Preserve any earlier caller deadline when it is shorter, and ensure the timeout
context is released after p.open and p.finishOpening complete.
🪄 Autofix
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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 81fb10a5-fd83-4269-82b9-cbabbd347faa
⛔ Files ignored due to path filters (2)
web/e2e/__tests__/os-shell.spec.tsis excluded by!web/e2e/**web/e2e/__tests__/session-hardening.spec.tsis excluded by!web/e2e/**
📒 Files selected for processing (2)
internal/store/sessiondb/read_only_pool.gointernal/store/sessiondb/session_db_integration_test.go
| openingCtx := context.WithoutCancel(ctx) | ||
| recorder, err := p.open(openingCtx, key.owner, key.path) | ||
| p.finishOpening(openingCtx, key, opening, recorder, err) |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
ast-grep outline internal/store/sessiondb/read_only_pool.go --items all
rg -n -C 5 'func \(p \*ReadOnlyPool\) (finishOpening|Close)|func openSessionDBReadOnlyWithRetry|context\.WithoutCancel|context\.WithDeadline|context\.WithTimeout|Deadline\(' internal/store/sessiondbRepository: compozy/compozy
Length of output: 19641
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== read_only_pool.go relevant sections =="
sed -n '1,130p' internal/store/sessiondb/read_only_pool.go
sed -n '156,260p' internal/store/sessiondb/read_only_pool.go
sed -n '258,330p' internal/store/sessiondb/read_only_pool.go
echo "== read_only_pool shutdown =="
sed -n '1,220p' internal/store/sessiondb/read_only_pool_shutdown.go
echo "== read_only.go open helper/retry =="
sed -n '1,240p' internal/store/sessiondb/read_only.go
echo "== all WithTimeout/WithDeadline/Deadline/context.WithoutCancel in files under internal/store/sessiondb =="
rg -n -C 4 'context\.WithoutCancel|context\.WithTimeout|context\.WithDeadline|Deadline\(|deadline|timeout|deadline=|ttl=' internal/store/sessiondb --glob '*.go'Repository: compozy/compozy
Length of output: 32667
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== OpenSessionDBReadOnly callers that set ReadOnlyPool Open option =="
rg -n -C 8 'OpenSessionDBReadOnly(\(|\s*|,|ReadOnlyPoolConfig|WithReadOnlyOpenRetry|--Open' internal --glob '*.go' | sed -n '1,240p'
echo "== NewReadOnlyPool Open config assignments =="
rg -n -C 6 'NewReadOnlyPool\(|ReadOnlyPoolConfig|Open:' internal --glob '*.go' | sed -n '1,260p'
echo "== read_only.go context usage and retry delay =="
sed -n '220,290p' internal/store/sessiondb/read_only.go
echo "== SQL context propagation around read-only opener path =="
rg -n -C 3 'MustContext|ExecContext|QueryRowContext|QueryContext|PrepareContext|Context\(\)$' internal/store/sessiondb/sqlcgen internal/store/sessiondb -g '*.go' | sed -n '1,220p'Repository: compozy/compozy
Length of output: 361
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== OpenSessionDBReadOnly callers from internal/ =="
rg -n -C 8 'OpenSessionDBReadOnly|WithReadOnlyOpenRetry|NewReadOnlyPool\(|ReadOnlyPoolConfig|Open:' internal --glob '*.go' | sed -n '1,340p'
echo "== read_only.go retry delay implementation =="
sed -n '220,290p' internal/store/sessiondb/read_only.go
echo "== read_only_pool.go constructor and config default path =="
sed -n '127,155p' internal/store/sessiondb/read_only_pool.goRepository: compozy/compozy
Length of output: 30221
Bounding the detached opener is needed for NewReadOnlyPool defaults.
context.WithoutCancel(ctx) removes the caller deadline and leaves p.open without an owner deadline. The built-in opener is OpenSessionDBReadOnly, which relies only on retry/backoff and SQLite busy handling and has no explicit total timeout. A blocked DB open while an entry is reserved can hold the pool indefinitely. Add an explicit timeout when the caller deadline is removed, or require injectable timeouts for the opener.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@internal/store/sessiondb/read_only_pool.go` around lines 213 - 215, Update
the opening flow around p.open and context.WithoutCancel so the detached opener
always has an explicit bounded timeout, including NewReadOnlyPool’s default
OpenSessionDBReadOnly path. Preserve any earlier caller deadline when it is
shorter, and ensure the timeout context is released after p.open and
p.finishOpening complete.
Source: Coding guidelines
Summary
dev-cycleextension enabled, while later boots and updates preserve an operator's explicit enabled or disabled choice.Closes #321.
Closes #322.
Root causes
InstallLocalManagedappended its disabled default after caller options, so the bundled dev-cycle installer could not opt intoenabled=true.loop-actionworker runs even after the dedicated executor had claimed them.requested/deliveringstate.ledger.jsonlprojection while new events accumulated, so the next stop could reject different content as an existing ledger conflict.Changes
dev-cycleinto fresh-home enablement, and retain stored state on reinstall/update.loop-actionexecutor authoritative.ErrHostedMCPUnavailablebefore provider startup when effective tools/toolsets require hosted MCP but the runtime mode, provider, launcher, or launch cannot provide it.Release Notes
🐛 Bug Fixes
QA
Final Status: PASS for the #321/#322 acceptance contract; all nine changed or adjacent product scenarios reached a terminal pass.
Coverage: Five personas exercised fresh extension lifecycle, real hosted Compozy tools, exclusive Loop-worker ownership, cancellation through draining to canceled, stopped-session continuation, and task-catalog refresh behavior.
Issues: 0 (Blocks-Completion: 0, Data-Loss: 0, Trust-Damage: 0, Friction: 0, Cosmetic: 0).
Blocked / needs human: None for the changed behavior. The separate broad business playbook remained blocked by missing local
psqland incomplete channel handoffs; those rows were not used as evidence for #321 or #322.Full report:
docs/qa/reports/2026-08-06-loop-agent-ownership.mdTest plan
make gate-full— full monorepo verification, 20,778 Go tests with-race, Bun lint/typecheck/test, web build, Go lint/build, codegen, installer check, and package boundaries.go test -race ./internal/session -count=1— 769 session tests.bunx turbo run typecheck test build --filter=./packages/site— 305 site tests and 2,012 generated static pages.clean: true, 0 surviving processes.Web / Docs Impact
Compozy Impact Audit
compozy__*ID, descriptor, schema, digest, risk flag, or toolset changed. Session availability now fails closed when the effective agent/lineage policy requires tools and hosted MCP cannot project them. Real QA invokedcompozy__tool_info,compozy__skill_view, and task tools through the hosted seam.dev-cycleinstalls are enabled; generic local and marketplace installs remain disabled by default, and stored extension state survives restart/update. Extension agents, Loops, and tools were verified across disable → restart → enable. Hooks, bridge SDKs, registries, MCP sidecar contracts, and config keys/defaults are unchanged.workspace_idand the Loop service revalidates run ownership before transition. Ledger discard/rematerialization resolves the immutable session/workspace owner. No new cross-workspace read or write surface was added.skills/compozy/: tool IDs, CLI paths, hook events, capability names, and agent-facing operating steps are unchanged; the public runtime docs own these lifecycle guarantees.Summary by CodeRabbit
New Features
Bug Fixes