- Delete safe_path entirely (following PR #483 approach) — no dead code
- Update all 3 READMEs per module to reflect read_file in Gate 2
- s04: also update permission_hook snippet in READMEs
- s20: remove safe_path (already replaced with inline resolution)
Closes#482
s04_hooks: permission_hook checks path + asks user, but safe_path
still raised hard ValueError — user approval was ineffective, same
root cause as s03 (#482).
s20_comprehensive: permission_hook used safe_path directly inside
try/except, silently denying all writes outside workspace without
ever asking the user. Now uses is_relative_to check + user prompt.
Both files also add read_file to the permission coverage.
s05-s08 intentionally NOT changed: their permission_hook does not
check paths at all — safe_path is their only path-safety defense.
safe_path hard-raised on out-of-workspace paths while Gate 2 asked about the same condition, so approving an out-of-workspace write never took effect. Make the permission pipeline the sole boundary authority: drop safe_path, resolve paths directly in the file tools, and extend the Gate 2 rule to read_file. Sync all three READMEs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
State the contribution rules for this teaching repository: tie each PR to an
issue, keep teaching code minimal, keep the three language READMEs in sync,
target current chapter files, and disclose AI assistance. Note that bulk PRs
generated by an agent across many repositories may be closed without detailed
review.
The code change dropped the redundant agent_name parameter, but the
trilingual README snippets still showed the old four-arg signature, the
double-passed call site, and agent_name inside the function body
(read_inbox and claim_task). Update all three language code blocks to
the three-arg form and bump the translation-sync markers so the docs
match the code.
The inbox poller added in #291 wakes the Lead on teammate inbox messages, but a
completed background task still strands its result: nothing wakes the Lead for it,
and collect_background_results() runs only inside the tool-use branch, so a
plain-text turn never picks it up. There is also no signal once every teammate has
finished.
Extend the same poller instead of adding a second path. has_pending_background()
reports, without consuming, whether any background task has completed. The poller
now wakes on unread inbox messages or a completed background task, and the wake
handler drains both the inbox and collect_background_results() before running the
turn, so background results become new turns without user input, the same way
teammate results already do. When the last teammate finishes and its output has
been drained, a single "[all teammates done]" marker is printed.
After the Lead spawns teammate agents, if the current round terminates with
plain text (stop_reason != tool_use), agent_loop returns and the REPL falls
back to a blocking input(). Teammates run as daemon threads and finish later,
so the results they send to the Lead's inbox stay unconsumed until the user
submits their next input.
The cause is that result delivery is bound to the turn loop, whose lifetime
ends when the model stops calling tools, while a teammate completes on its own
clock. Blocking a teammate is harmless because it runs as a background thread;
the problem is specific to the Lead, which owns the user prompt.
The fix follows real Claude Code (useInboxPoller, described in the appendix):
decouple input() from turn execution. MessageBus.peek(agent) reports whether
the inbox has unread messages without consuming them. In __main__, input()
moves to a dedicated daemon thread and an inbox poller thread peeks the Lead's
inbox every second; both push to one shared event queue. The main thread runs
one turn per event, woken by either user input or an incoming teammate message,
so teammate results become new turns without waiting for the user. Repeated
inbox wakeups are idempotent: an empty read is skipped when a prior read_inbox
already drained the messages. Only the Lead's main loop changes; the teammates'
idle loops are untouched.
Match the snip_compact teaching snippet (en/zh/ja) to code.py: add the
`head_end > 0` head guard and the `tail_start > 0 and tail_start <
len(messages)` tail bounds check before indexing messages, mirroring the
reactive_compact alignment.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011UJumwVeBXSGYh8nSzoVTF
The reactive_compact README snippets (en/zh/ja) used a shortened tail
boundary check, while code.py guards with `tail_start > 0 and tail_start
< len(messages)` before reading messages[tail_start]. Match the snippets
to the actual code so the teaching example stays consistent.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011UJumwVeBXSGYh8nSzoVTF
reactive_compact summarized the full message history before slicing off
the recent tail, so the kept tail was summarized and then re-attached
verbatim, and the emergency prompt-too-long path re-summarized the whole
oversized context. Move the summarize_history call below the tail
computation (including the tool_use/tool_result pair adjustment) and
summarize only messages[:tail_start], so the summary covers older history
while the recent tail stays verbatim.
Applied to the duplicated function in s08, s09, and s20, with the s08
README snippets (en/zh/ja) updated to match. Adds tests covering the
normal path and the case where a tool pair straddles the tail boundary.
Fixes#350
Make the Storage to Memory Files arrow a straight vertical segment
ending at the box edge, instead of curving onto the dashed border.
Applies to the en, ja, and zh (default) diagram variants.