Skip to content

fix(apps): keep the app-config writes off the event loop - #4550

Open
leonlaiyc wants to merge 2 commits into
kirodotdev:mainfrom
leonlaiyc:fix/apps-config-writes-off-loop
Open

fix(apps): keep the app-config writes off the event loop#4550
leonlaiyc wants to merge 2 commits into
kirodotdev:mainfrom
leonlaiyc:fix/apps-config-writes-off-loop

Conversation

@leonlaiyc

@leonlaiyc leonlaiyc commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Problem / Motivation

PUT /api/apps/registries performed its config.json read-modify-write synchronously on the asyncio event loop and without holding both config-writer lock generations. A concurrent writer could therefore lose unrelated configuration.

The trust audit also had two opposite failure modes:

  • auditing before persistence could claim a trust grant after a failed write;
  • auditing after run_config_write in the async handler could be skipped on cancellation even though the shielded worker completed the write.

Current main already contains the earlier enable/disable offload work. This PR is intentionally limited to the remaining registry PUT path.

What changed

  • Move the complete registry read-modify-write into _write_registries_config.
  • Dispatch it through dashboard.chat_utils.run_config_write, which holds the loop-side async config lock while the worker uses update_config_locked and its cross-process sidecar lock.
  • Compute newly trusted hosts from the state protected by that locked mutation.
  • Emit each registries.host_trust_granted event in the same worker immediately after the write commits. Failed writes emit no grant, while cancellation can no longer leave persisted trust without an audit record.
  • Preserve unrelated config, existing error contracts, grant deduplication, and the request-level registries.update event.
  • Keep the run_config_write import call-time. This avoids introducing the first module-scope dependency from apps into dashboard and avoids loading the dashboard tree for routes that never write config.

The previous unrelated SessionManager teardown rider was removed in full while resolving the merge conflict. The PR now contains exactly two cohesive commits and changes only:

  • src/kiro_crew/apps/routes.py
  • test/test_apps_instances_loop_offload.py

Concurrency and cancellation contract

config.json currently has two writer generations:

  • update_config_locked takes the cross-process advisory lock;
  • legacy dashboard writers serialize on the in-process async config lock.

Holding only one permits stale-snapshot overwrites across the two families. run_config_write is the existing bridge that holds both while keeping blocking I/O off the event loop. It shields and drains the worker across cancellation, so the grant audit must live with the worker's committed mutation rather than in the cancelled caller.

Tests

The focused file now has 23 deterministic tests covering:

  • worker-thread dispatch and loop responsiveness;
  • ownership and release of the loop-side lock;
  • locked mutation, unrelated-config preservation, and grant deduplication;
  • failed writes producing neither persistence nor a trust-grant audit;
  • cancellation at a controlled threading.Event boundary still producing both the committed config and exactly one grant audit;
  • AST ratchets preventing a bare asyncio.to_thread regression;
  • cancellation of another blocked registry write using events and an event-loop sentinel, replacing its former sleep(0.05) timing guesses.

Validation on the pushed head:

  • test/test_apps_instances_loop_offload.py: 23 passed under strict RuntimeWarning and PytestUnraisableExceptionWarning handling;
  • mypy on both changed files: pass;
  • flake8 and isort on both changed files: pass;
  • official Black changed-file gate: pass;
  • import smoke test, git diff --check, and merge-tree against the then-current main (901ef09): pass.

No retries, warning filters, relaxed assertions, longer acceptance timeouts, or scheduler-time sleeps were added.

Related pending PRs

The shared apps/routes.py changes in #6854 and #5488 are in separate semantic regions. #6206 is a broader 27-file App Store refresh PR that is itself conflicting; this smaller config-writer correctness fix is resolved first so #6206 can reconcile against the stable owner.

Checklist

  • At most two commits (this PR has exactly two)
  • Merge conflict resolved without restoring unrelated changes
  • Deterministic targeted tests cover the behavior
  • Static and formatting gates pass
  • No secrets, credentials, or internal references in the diff

Contribution License Agreement

@leonlaiyc
leonlaiyc requested a review from a team as a code owner August 19, 2026 14:32
@github-actions github-actions Bot added fork Pull request from a fork (external contributor) readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running and removed readiness: action required A blocking check or review needs attention labels Aug 19, 2026
@leonlaiyc
leonlaiyc force-pushed the fix/apps-config-writes-off-loop branch from da5ba08 to 81ec689 Compare August 19, 2026 15:15
@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Aug 19, 2026
leonlaiyc added a commit to leonlaiyc/KiroCrew that referenced this pull request Aug 23, 2026
Every config.json update in dashboard/handlers/memory.py reads the file,
changes one key and writes it all back on the event loop. The read, the
JSON parse, and write_config_atomically -- a tmp-file write plus a rename,
which can fsync -- all block, stalling every other session while they run.
Three sites do it: api_memory_settings, _write_embed_model_config, and
_set_migrated, which runs on EVERY boot while migrated is false and so
lands the stall exactly when the gateway is bringing sessions up. This is
the class the repo has been closing site by site (kirodotdev#4118, kirodotdev#3803, kirodotdev#4550), and
kirodotdev#4946's review named this module.

The whole transaction crosses over, never just the read. Offloading the
read alone would leave the write on the loop and insert a suspension point
between the read and the write-back while the file is unguarded on disk: an
external editor, a CLI command or another process landing in that gap would
be silently overwritten by a write derived from state nobody re-checked.
That gap is zero today because the sequence is synchronous, and it stays
zero because the worker performs the whole thing without yielding. The
existing per-config lock is held across the hop, so two coroutines still
cannot interleave.

Two of the three sites also hand-rolled a reader this module already
imports. read_config_for_update is the documented companion to
write_config_atomically with 27 call sites, and api_memory_settings uses it
200 lines above; _set_migrated and _write_embed_model_config instead caught
Exception around json.loads. The helper additionally refuses a non-object
top level, where the hand-rolled version accepted a list and then raised
AttributeError from setdefault -- a crash where a fail-closed refusal was
intended.

ConfigReadError is not swallowed by the helper: what to tell the user
differs per site, and each keeps exactly the behaviour it had -- skip and
retry next boot, raise ValueError, or answer 500 config_unreadable.

api_memory_settings now validates its body before the transaction. None of
that reads the config, and a 400 previously took the lock and abandoned it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
leonlaiyc added a commit to leonlaiyc/KiroCrew that referenced this pull request Aug 23, 2026
_write_env_updates stats and reads the whole .env, re-parses it line by
line, then creates a 0600 temp file, chmods it, writes and renames. All
synchronous file I/O, and six async channel config-save handlers call it.

Three already reached it through asyncio.to_thread -- telegram, teams,
wecom -- and three called it inline on the gateway loop: slack, discord and
webex, stalling every other session for the duration of a token save.

So this is not a missing convention but an existing one applied to half the
call sites. messaging.py already uses asyncio.to_thread 29 times, and with
three siblings doing it correctly nothing in the file said which half was
right, or stopped the next channel from copying the wrong one. It is the
class the repo has been closing site by site (kirodotdev#4118, kirodotdev#3803, kirodotdev#4550), and
kirodotdev#4946's review named this module.

The WHOLE call is offloaded, never a part of it: the read-modify-write is
one transaction, and a suspension point between the read and the rename
would let a concurrent writer's keys be dropped by a write derived from
lines nobody re-read. Keeping _write_env_updates one synchronous function
on one worker preserves that without depending on the caller, which is now
said on the function itself so the next channel inherits the reason and not
just the shape.

The regression pins all six channels rather than the three that moved,
since the defect was the split and not any one site.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions github-actions Bot added the merge conflict Branch has merge conflicts with its base — author must resolve before merge label Aug 23, 2026
leonlaiyc added a commit to leonlaiyc/KiroCrew that referenced this pull request Aug 24, 2026
Every config.json update in dashboard/handlers/memory.py reads the file,
changes one key and writes it all back on the event loop. The read, the
JSON parse, and write_config_atomically -- a tmp-file write plus a rename,
which can fsync -- all block, stalling every other session while they run.
Three sites do it: api_memory_settings, _write_embed_model_config, and
_set_migrated, which runs on EVERY boot while migrated is false and so
lands the stall exactly when the gateway is bringing sessions up. This is
the class the repo has been closing site by site (kirodotdev#4118, kirodotdev#3803, kirodotdev#4550), and

The whole transaction crosses over, never just the read. Offloading the
read alone would leave the write on the loop and insert a suspension point
between the read and the write-back while the file is unguarded on disk: an
external editor, a CLI command or another process landing in that gap would
be silently overwritten by a write derived from state nobody re-checked.
That gap is zero today because the sequence is synchronous, and it stays
zero because the worker performs the whole thing without yielding. The
existing per-config lock is held across the hop, so two coroutines still
cannot interleave.

Two of the three sites also hand-rolled a reader this module already
imports. read_config_for_update is the documented companion to
write_config_atomically with 27 call sites, and api_memory_settings uses it
200 lines above; _set_migrated and _write_embed_model_config instead caught
Exception around json.loads. The helper additionally refuses a non-object
top level, where the hand-rolled version accepted a list and then raised
AttributeError from setdefault -- a crash where a fail-closed refusal was
intended.

ConfigReadError is not swallowed by the helper: what to tell the user
differs per site, and each keeps exactly the behaviour it had -- skip and
retry next boot, raise ValueError, or answer 500 config_unreadable.

api_memory_settings now validates its body before the transaction. None of
that reads the config, and a 400 previously took the lock and abandoned it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@bolichen97
bolichen97 enabled auto-merge (squash) August 24, 2026 06:58
leonlaiyc added a commit to leonlaiyc/KiroCrew that referenced this pull request Aug 24, 2026
Every config.json update in dashboard/handlers/memory.py reads the file,
changes one key and writes it all back on the event loop. The read, the
JSON parse, and write_config_atomically -- a tmp-file write plus a rename,
which can fsync -- all block, stalling every other session while they run.
Three sites do it: api_memory_settings, _write_embed_model_config, and
_set_migrated, which runs on EVERY boot while migrated is false and so
lands the stall exactly when the gateway is bringing sessions up. This is
the class the repo has been closing site by site (kirodotdev#4118, kirodotdev#3803, kirodotdev#4550), and

The whole transaction crosses over, never just the read. Offloading the
read alone would leave the write on the loop and insert a suspension point
between the read and the write-back while the file is unguarded on disk: an
external editor, a CLI command or another process landing in that gap would
be silently overwritten by a write derived from state nobody re-checked.
That gap is zero today because the sequence is synchronous, and it stays
zero because the worker performs the whole thing without yielding. The
existing per-config lock is held across the hop, so two coroutines still
cannot interleave.

Two of the three sites also hand-rolled a reader this module already
imports. read_config_for_update is the documented companion to
write_config_atomically with 27 call sites, and api_memory_settings uses it
200 lines above; _set_migrated and _write_embed_model_config instead caught
Exception around json.loads. The helper additionally refuses a non-object
top level, where the hand-rolled version accepted a list and then raised
AttributeError from setdefault -- a crash where a fail-closed refusal was
intended.

ConfigReadError is not swallowed by the helper: what to tell the user
differs per site, and each keeps exactly the behaviour it had -- skip and
retry next boot, raise ValueError, or answer 500 config_unreadable.

api_memory_settings now validates its body before the transaction. None of
that reads the config, and a 400 previously took the lock and abandoned it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
leonlaiyc added a commit to leonlaiyc/KiroCrew that referenced this pull request Aug 24, 2026
Every config.json update in dashboard/handlers/memory.py reads the file,
changes one key and writes it all back on the event loop. The read, the
JSON parse, and write_config_atomically -- a tmp-file write plus a rename,
which can fsync -- all block, stalling every other session while they run.
Three sites do it: api_memory_settings, _write_embed_model_config, and
_set_migrated, which runs on EVERY boot while migrated is false and so
lands the stall exactly when the gateway is bringing sessions up. This is
the class the repo has been closing site by site (kirodotdev#4118, kirodotdev#3803, kirodotdev#4550), and

The whole transaction crosses over, never just the read. Offloading the
read alone would leave the write on the loop and insert a suspension point
between the read and the write-back while the file is unguarded on disk: an
external editor, a CLI command or another process landing in that gap would
be silently overwritten by a write derived from state nobody re-checked.
That gap is zero today because the sequence is synchronous, and it stays
zero because the worker performs the whole thing without yielding. The
existing per-config lock is held across the hop, so two coroutines still
cannot interleave.

Two of the three sites also hand-rolled a reader this module already
imports. read_config_for_update is the documented companion to
write_config_atomically with 27 call sites, and api_memory_settings uses it
200 lines above; _set_migrated and _write_embed_model_config instead caught
Exception around json.loads. The helper additionally refuses a non-object
top level, where the hand-rolled version accepted a list and then raised
AttributeError from setdefault -- a crash where a fail-closed refusal was
intended.

ConfigReadError is not swallowed by the helper: what to tell the user
differs per site, and each keeps exactly the behaviour it had -- skip and
retry next boot, raise ValueError, or answer 500 config_unreadable.

api_memory_settings now validates its body before the transaction. None of
that reads the config, and a 400 previously took the lock and abandoned it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
leonlaiyc added a commit to leonlaiyc/KiroCrew that referenced this pull request Aug 25, 2026
Every config.json update in dashboard/handlers/memory.py reads the file,
changes one key and writes it all back on the event loop. The read, the
JSON parse, and write_config_atomically -- a tmp-file write plus a rename,
which can fsync -- all block, stalling every other session while they run.
Three sites do it: api_memory_settings, _write_embed_model_config, and
_set_migrated, which runs on EVERY boot while migrated is false and so
lands the stall exactly when the gateway is bringing sessions up. This is
the class the repo has been closing site by site (kirodotdev#4118, kirodotdev#3803, kirodotdev#4550), and

The whole transaction crosses over, never just the read. Offloading the
read alone would leave the write on the loop and insert a suspension point
between the read and the write-back while the file is unguarded on disk: an
external editor, a CLI command or another process landing in that gap would
be silently overwritten by a write derived from state nobody re-checked.
That gap is zero today because the sequence is synchronous, and it stays
zero because the worker performs the whole thing without yielding. The
existing per-config lock is held across the hop, so two coroutines still
cannot interleave.

Two of the three sites also hand-rolled a reader this module already
imports. read_config_for_update is the documented companion to
write_config_atomically with 27 call sites, and api_memory_settings uses it
200 lines above; _set_migrated and _write_embed_model_config instead caught
Exception around json.loads. The helper additionally refuses a non-object
top level, where the hand-rolled version accepted a list and then raised
AttributeError from setdefault -- a crash where a fail-closed refusal was
intended.

ConfigReadError is not swallowed by the helper: what to tell the user
differs per site, and each keeps exactly the behaviour it had -- skip and
retry next boot, raise ValueError, or answer 500 config_unreadable.

api_memory_settings now validates its body before the transaction. None of
that reads the config, and a 400 previously took the lock and abandoned it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
iamwhatever pushed a commit that referenced this pull request Aug 25, 2026
…5059)

Every config.json update in dashboard/handlers/memory.py reads the file,
changes one key and writes it all back on the event loop. The read, the
JSON parse, and write_config_atomically -- a tmp-file write plus a rename,
which can fsync -- all block, stalling every other session while they run.
Three sites do it: api_memory_settings, _write_embed_model_config, and
_set_migrated, which runs on EVERY boot while migrated is false and so
lands the stall exactly when the gateway is bringing sessions up. This is
the class the repo has been closing site by site (#4118, #3803, #4550), and

The whole transaction crosses over, never just the read. Offloading the
read alone would leave the write on the loop and insert a suspension point
between the read and the write-back while the file is unguarded on disk: an
external editor, a CLI command or another process landing in that gap would
be silently overwritten by a write derived from state nobody re-checked.
That gap is zero today because the sequence is synchronous, and it stays
zero because the worker performs the whole thing without yielding. The
existing per-config lock is held across the hop, so two coroutines still
cannot interleave.

Two of the three sites also hand-rolled a reader this module already
imports. read_config_for_update is the documented companion to
write_config_atomically with 27 call sites, and api_memory_settings uses it
200 lines above; _set_migrated and _write_embed_model_config instead caught
Exception around json.loads. The helper additionally refuses a non-object
top level, where the hand-rolled version accepted a list and then raised
AttributeError from setdefault -- a crash where a fail-closed refusal was
intended.

ConfigReadError is not swallowed by the helper: what to tell the user
differs per site, and each keeps exactly the behaviour it had -- skip and
retry next boot, raise ValueError, or answer 500 config_unreadable.

api_memory_settings now validates its body before the transaction. None of
that reads the config, and a 400 previously took the lock and abandoned it.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@dwu96 dwu96 added the needs-pr-triage PR scanner: awaiting automated triage label Aug 26, 2026
@iamwhatever iamwhatever added drive-to-green PR claimed by drive-to-green pipeline and removed needs-pr-triage PR scanner: awaiting automated triage labels Aug 26, 2026
@iamwhatever

Copy link
Copy Markdown
Collaborator

🤖 Kiro Crew [operator: iamwhatever]: This PR has been inactive for 7+ days with failing CI. I've assessed the blockers and they appear resolvable — I'll push fixes directly to this branch as a co-author.

Assessment: Two mechanical blockers, neither in the PR's own logic. (1) The branch is CONFLICTING against main — both touched files moved after this head (src/kiro_crew/apps/routes.py last via #6007 app-trust-to-repository binding; test/test_apps_instances_loop_offload.py via #5676/#5220), so the fix is a rebase taking main's structure and layering this PR's asyncio.to_thread offload + replace_with_retry adoption + extended AST-ratchet banned set on top. (2) The only red check, E2E (stub ACP backend, offline), concluded cancelled on 2026-08-19 with no output — a superseded run, not a real failure; it re-runs on the rebased head. No review threads or reviewer concerns exist on this PR.

If you'd prefer I don't touch this PR, add the pr-no-autofix label.

@bolichen97
bolichen97 force-pushed the fix/apps-config-writes-off-loop branch from 81ec689 to bf0ae7c Compare August 30, 2026 01:51
@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: action required A blocking check or review needs attention labels Aug 30, 2026
@github-actions github-actions Bot added the readiness: checking Automated validation is still running label Aug 30, 2026
@leonlaiyc

Copy link
Copy Markdown
Contributor Author

Pushed 9141210ed. Dispositions for the three findings on bf78ff557:

1. BLOCKING — cancellation persists a trust grant without auditing it: ACCEPTED, fixed.
Confirmed from run_config_write itself, not from the review text: it shields the worker, drains it in a loop, then raise asyncio.CancelledError and discards result. So the write commits and the caller-side audit is skipped on exactly the path where the grant landed. The suggested fix was the right one — the per-host event now emits inside _write_registries_config, immediately after update_config_locked returns. Three things were checked before moving it: ordering (still strictly after the commit, so a failed write announces nothing), thread-safety (SEL's _on_event_loop exists to adapt, and off-loop is the preferred caller), and blast radius (a non-critical event swallows filesystem errors, so an audit failure cannot turn a committed write into a 500). Red-before parks the worker inside the locked write on an event, cancels there, and reproduces the defect as config committed, 0 grants.

2. FINDING — function-local run_config_write import: correct on the narrow point, but not hoisted; the comment is fixed instead.
You are right that the circular-import exemption does not apply — there is no cycle. I checked every import order (dashboard.chat_utils, dashboard.handlers.agents, dashboard.server, apps, apps.routes, kiro_crew) with the hoist applied and all import cleanly. The comment implied an exemption it does not have, so the comment was wrong and is now corrected. The import stays lazy for two reasons that hold independently, both measurable:

  • No module in src/kiro_crew/apps/ imports kiro_crew.dashboard at module scope — not one. Hoisting creates the package's first load-time edge in the wrong direction, into a package that already imports back into this one.
  • Hoisting pulls 117 extra modules into every importer of apps.routes (479 → 596).

top-level-imports is blocking: false, and the repo's own precedent (auto-improvement-test-plan.md D-76) is that a measured load cost is a valid reason to decline a hoist.

3. FINDING — session.py:5031 outside the stated scope: accurate, but not mine to revert.
That commit (bf78ff557, "fix: await session teardown tasks") is @bolichen97's, pushed after a rebase of this branch. I will not force-push over another contributor's work. What I could fix is the description, which claimed the branch "applies only the remaining registry fix" — it no longer does; the body now declares both writers and their commits explicitly.

Worth flagging for whoever owns that commit: the merge conflict is entirely in that rider. Test-merging this head against current main conflicts only in src/kiro_crew/session.py; apps/routes.py and test/test_session.py auto-merge cleanly. Splitting the session-teardown commit into its own PR would address both reviews' scope concern and unblock this merge in one move.

Also noted, deliberately not taken: the First Principles review's two _sync_builtin_config siblings (routes.py:1597, :1693) are real — same main config.json, same one-lock-only gap. Not folded in: both sit inside async with app_lifecycle_lock(name), so routing them through run_config_write nests _get_config_lock inside that lock and needs a lock-ordering proof first. That is its own PR with its own test, not a widening of this one.

@bolichen97
bolichen97 force-pushed the fix/apps-config-writes-off-loop branch from 9141210 to 7885b24 Compare August 30, 2026 08:52
@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Aug 30, 2026
@dwu96

dwu96 commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

👋 Hi! This PR's description is missing some required sections from our PR template. Workflow runs won't be auto-approved until the description is updated.

Missing sections:

  • ## Why it matters

Please update your PR description to include these sections, then push or re-save the description. The workflows will be approved on the next cycle.

@iamwhatever

Copy link
Copy Markdown
Collaborator

👋 Hi! This PR's description is missing some required sections from our PR template. Workflow runs won't be auto-approved until the description is updated.

Missing sections:

  • ## Why it matters

Please update your PR description to include these sections, then push or re-save the description. The workflows will be approved on the next cycle.

@github-actions github-actions Bot removed the merge conflict Branch has merge conflicts with its base — author must resolve before merge label Aug 30, 2026
@dwu96

dwu96 commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

👋 Hi! This PR's description is missing some required sections from our PR template. Workflow runs won't be auto-approved until the description is updated.

Missing sections:

  • ## Why it matters

Please update your PR description to include these sections, then push or re-save the description. The workflows will be approved on the next cycle.

4 similar comments
@dwu96

dwu96 commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

👋 Hi! This PR's description is missing some required sections from our PR template. Workflow runs won't be auto-approved until the description is updated.

Missing sections:

  • ## Why it matters

Please update your PR description to include these sections, then push or re-save the description. The workflows will be approved on the next cycle.

@iamwhatever

Copy link
Copy Markdown
Collaborator

👋 Hi! This PR's description is missing some required sections from our PR template. Workflow runs won't be auto-approved until the description is updated.

Missing sections:

  • ## Why it matters

Please update your PR description to include these sections, then push or re-save the description. The workflows will be approved on the next cycle.

@dwu96

dwu96 commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

👋 Hi! This PR's description is missing some required sections from our PR template. Workflow runs won't be auto-approved until the description is updated.

Missing sections:

  • ## Why it matters

Please update your PR description to include these sections, then push or re-save the description. The workflows will be approved on the next cycle.

@dwu96

dwu96 commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

👋 Hi! This PR's description is missing some required sections from our PR template. Workflow runs won't be auto-approved until the description is updated.

Missing sections:

  • ## Why it matters

Please update your PR description to include these sections, then push or re-save the description. The workflows will be approved on the next cycle.

@dwu96

dwu96 commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

👋 Hi! This PR's description is missing some required sections from our PR template. Workflow runs won't be auto-approved until the description is updated.

Missing sections:

  • ## Why it matters

Please update your PR description to include these sections, then push or re-save the description. The workflows will be approved on the next cycle.

4 similar comments
@dwu96

dwu96 commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

👋 Hi! This PR's description is missing some required sections from our PR template. Workflow runs won't be auto-approved until the description is updated.

Missing sections:

  • ## Why it matters

Please update your PR description to include these sections, then push or re-save the description. The workflows will be approved on the next cycle.

@dwu96

dwu96 commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

👋 Hi! This PR's description is missing some required sections from our PR template. Workflow runs won't be auto-approved until the description is updated.

Missing sections:

  • ## Why it matters

Please update your PR description to include these sections, then push or re-save the description. The workflows will be approved on the next cycle.

@dwu96

dwu96 commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

👋 Hi! This PR's description is missing some required sections from our PR template. Workflow runs won't be auto-approved until the description is updated.

Missing sections:

  • ## Why it matters

Please update your PR description to include these sections, then push or re-save the description. The workflows will be approved on the next cycle.

@dwu96

dwu96 commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

👋 Hi! This PR's description is missing some required sections from our PR template. Workflow runs won't be auto-approved until the description is updated.

Missing sections:

  • ## Why it matters

Please update your PR description to include these sections, then push or re-save the description. The workflows will be approved on the next cycle.

@bolichen97

Copy link
Copy Markdown
Collaborator

Open PR relationship audit

This is a consolidated, point-in-time code-level audit note. It compares complete merge-base diffs and current/merged code; it does not treat a shared topic as duplication or partial coverage as completion.

Relationship findings

  • PR #7167 is OVERLAPPING relative to this PR. The goals differ or the implementations can complement each other; this is not a duplicate claim. Recommended action for PR #7167: REBASE. Independent call site; landing either one does nothing for the other. Files: src/kiro_crew/apps/routes.py.
  • This PR is PARTIALLY_COVERED with PR #6984. Coverage is explicitly incomplete; this finding is not a completion or closure claim. Recommended action for PR #4550: KEEP. PR #6984 landed the sibling half in the same file; the registry PUT remains the uncovered half and this PR was already narrowed to exactly it, so the two are complementary rather than redundant. Files: src/kiro_crew/apps/routes.py.

No PR, Issue, label, branch, or review state was changed by the relationship-note portion of this audit.

@dwu96

dwu96 commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

👋 Hi! This PR's description is missing some required sections from our PR template. Workflow runs won't be auto-approved until the description is updated.

Missing sections:

  • ## Why it matters

Please update your PR description to include these sections, then push or re-save the description. The workflows will be approved on the next cycle.

leonlaiyc and others added 2 commits September 8, 2026 09:07
Current main already offloads the enable and disable writers from the
original change. Complete the remaining registry PUT path by moving its
entire read-modify-write off the loop and using the shared locked config
updater. Compute newly trusted hosts from the state protected by that
lock, preserve unrelated config, deduplicate grants, and emit trust
events only after the write commits.

config.json has two writer generations that do not exclude each other:
update_config_locked takes the sidecar advisory flock, while the legacy
dashboard writers (agents endpoint, updates.py, security.py,
messaging.py, mcp.py, core.py STT) serialize on the loop-side
_get_config_lock alone. Dispatch the registry write through
dashboard.chat_utils.run_config_write, the one entry point that holds
both, so a concurrent legacy PUT cannot commit a stale snapshot over it.
The blocking work still runs in a worker, so the loop never stalls.

Add deterministic execution-level and AST coverage for worker-thread
dispatch, loop-side lock ownership across the write, locked mutation,
preserved data, grant deduplication, and failed-write audit behavior.
run_config_write shields its worker and drains it across cancellation, then
re-raises CancelledError and discards the return value. A gateway shutdown or
a client disconnect landing while the registry PUT's write is in flight
therefore committed the trust grant and never reached the caller-side audit:
persisted trust with no registries.host_trust_granted record, which is exactly
the reconstruction gap that event exists to close.

Emit the per-host grant inside _write_registries_config, immediately after
update_config_locked returns. Nothing can interrupt a thread between those two
statements, so the event and the commit cannot disagree; the emission is still
strictly after the write, so a failed write announces nothing; and SEL adapts
to an off-loop caller and swallows filesystem errors on a non-critical event,
so this cannot turn a committed write into a 500. The handler keeps the
registries.update API-outcome event, which correctly belongs to the request.

Three tests: a cancellation delivered while the worker is parked inside the
locked write (red-before: config committed, zero grants); exactly-once
auditing per newly trusted host; and no grant when the locked write raises.

Also corrects the run_config_write import comment, which implied the
top-level-imports circular-import exemption. There is no cycle on that edge --
every import order was checked. The real reasons are layering and load cost,
both now stated and checkable: no module in apps/ imports dashboard at module
scope, and hoisting pulls 117 extra modules into every importer of apps.routes
(479 -> 596). The import stays lazy; the justification is now accurate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@bolichen97
bolichen97 force-pushed the fix/apps-config-writes-off-loop branch from 7885b24 to 0eb7742 Compare September 8, 2026 09:34
@bolichen97

Copy link
Copy Markdown
Collaborator

Rebased onto main 6fbb06bc by a maintainer as part of the 2026-09-08 open-PR audit.

Clean rebase — no conflicts. The only delta from your old head is one stray ) line that main's current shape already carried, so the diff is 525/-46 instead of 526/-47. Behaviour is unchanged.

Gates run locally on the rebased head: black --check on the two changed files (test/test_apps_instances_loop_offload.py is already in .github/black-baseline.txt and was equally unformatted on your old head, so nothing new), isort --check-only clean, flake8 clean, and pytest test/test_apps_instances_loop_offload.py → 23 passed.

Please review the rebased head. A maintainer push makes the maintainer the last pusher, so under this repo's last-push rule a second approver is needed. Reply here if anything looks wrong.

@dwu96

dwu96 commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

👋 Hi! This PR's description is missing some required sections from our PR template. Workflow runs won't be auto-approved until the description is updated.

Missing sections:

  • ## Why it matters

Please update your PR description to include these sections, then push or re-save the description. The workflows will be approved on the next cycle.

1 similar comment
@dwu96

dwu96 commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

👋 Hi! This PR's description is missing some required sections from our PR template. Workflow runs won't be auto-approved until the description is updated.

Missing sections:

  • ## Why it matters

Please update your PR description to include these sections, then push or re-save the description. The workflows will be approved on the next cycle.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

drive-to-green PR claimed by drive-to-green pipeline fork Pull request from a fork (external contributor) readiness: action required A blocking check or review needs attention

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants