Skip to content

[hosting] Hosted progress events: the events plug implementation and its emit points - #2138

Merged
chaojhou merged 14 commits into
mainfrom
hosting/h2-01-events
Oct 26, 2026
Merged

chaojhou merged 14 commits into
mainfrom
hosting/h2-01-events

Conversation

@zoroyihan7

@zoroyihan7 zoroyihan7 commented Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator
  • Description: the hosted implementation of the events plug (src/hyperloom/hosted/events.py) and the emit points in the legacy flow. The plug kernel and the local implementation are already on main (feat(plugs): add the plug kernel, built-in profiles and the events socket #2110). The hosted implementation writes CloudEvents 1.0 structured JSON, one event per line, to <events dir>/<10-digit segment>.jsonl:
    • id is a decimal string, strictly increasing within the run, resumed from the last complete line of the last segment on start; a half-written last line (a crashed write) is cut off first, so the next event never glues onto it.
    • Every line is written whole, in one write, and ends with a newline, so a reader that tails by byte offset can tell an unfinished line from an event.
    • A segment rolls before it would pass 4 MiB; fsync at most once a second and on flush()/close; one event is at most 16 KiB (larger content is replaced by {"oversized": true, "bytes", "keys"} with a warning); thread-safe; a failed write is logged, the partial bytes are removed, and the run continues.
    • One writer per events directory: the directory and run id reach the implementation from HostInfo through the plug context, never from os.environ, which take_host_info has already cleared. A child process therefore has nothing to write to.
    • Emit points (each one call at an existing write point, through hyperloom/plugs/progress.py, which never raises): phase_changed in orchestrator/phases/machine.py; candidate_settled in breakdown/recorder/phase_event.py record_settle; best_updated in orchestrator/loop/_current_best_lift.py _write_current_best; budget after SharedState.save(), at most every 5 minutes (tokens and gpu_sec are null, the legacy flow keeps no total of either); warning at three degradation paths (state save failure at a phase boundary, a vetoed winner (_refuses_graded_winner in the same file), a recorder write failure). plugs_lock_mismatch is already emitted by the plug host (feat(plugs): add the plug kernel, built-in profiles and the events socket #2110) and is reused.
    • Data schema: src/hyperloom/contracts/hyperloom_events.v1.schema.json (v1 type set only, shipped as package data); a test validates what the emitters send against it. measurement_* and node_quarantined are defined in the schema only; their emitters come with hosted measurement.
    • Profiles: hosted-cpu and hosted-gpu select events=hosted and no longer carry config.dir = ${HYPERLOOM_EVENTS_DIR} (that variable is removed from the environment before the plug host expands it; the directory comes from HostInfo). The built-in registry gains events/hosted. The other hosted-* sockets still have no implementation on main, so those profiles are refused exactly as before.
    • How the hosted entry calls it: campaign_started, campaign_finished and campaign_failed are emitted by the entry (not here). After take_host_info and building the PlugHost with host_info=<HostInfo>, the entry runs inside activated(host) and calls hyperloom.plugs.progress.emit("hyperloom.campaign_started", {"session_dir", "profile", "plugs_fingerprint"}), and likewise campaign_finished (stop_reason, best, report_path) or campaign_failed (error, optional stop_reason), then host.get("events").flush() before host.close() (close also syncs). emit never raises.
  • Linked issue(s): hosting board line H2-01
  • Tests: hosted/tests/test_hosted_events.py (CloudEvents attributes on every type, id resume across a restart including a half-written last line and an empty last segment, rollover at 4 MiB, 8 threads x 150 events with unique increasing ids in file order, oversized event, fsync rate bound with a fake clock, failed and partial write leaves no partial line, a failed os.open during a segment roll is retried by the next emit (the next id, in the new segment), a real child process has no events directory variable, the directory comes from the host inputs and not from a decoy in the environment); plugs/tests/test_progress.py (emitters match the schema, schema covers exactly the v1 types, the plug host's lock mismatch event matches the schema, an emit that raises does not reach the caller, budget throttle); orchestrator/phases/tests/test_progress_emit_points.py (phase switch, best_updated, no event for a refused winner); one test added to test_sbd_v6_phase_timeline.py (candidate_settled). Reverse-proofs run once each: resume ignoring the last id fails 2 tests; reading the directory from the environment fails the decoy test; take_host_info leaving the variable in place fails the child test; writing without the newline fails 8 tests; closing the current segment before the next one opens fails the roll-retry test. Ran ruff check and format, lint-imports, and the phases, loop, breakdown, plugs, hosted, state and inference_optimizer test trees.
  • Size/complexity triggers crossed: none. shared_state.py is over 1200 lines and does not grow: one function-local import of a name already imported at module level was removed to pay for the added lines. This PR does not touch writeback.py or the code-metrics baseline. No function gets longer than its recorded value. Segment files are created with mode 0600 (owner only).
  • If this simplifies or refactors: no.
  • Observable effect: none in a local run (the local implementation only logs at debug). In a hosted run the events directory now holds the progress stream the platform tails. docs/reference/environment-variables.md documents the format.
  • Breaking changes: no. The hosted-* profile files drop events.config.dir; those profiles were not usable on main.
  • PR addresses single concern: yes
  • Root cause is upstream (Magpie/TraceLens/GEAK/IntelliKit/AgentKernelArena), ticket filed: no

🤖 Generated with Claude Code

zoroyihan7 and others added 3 commits October 23, 2026 09:31
CloudEvents 1.0 lines in rolling segment files with a resumed, strictly
increasing id, fsync at most once a second, one writer per directory taken
from the host inputs. The legacy flow emits phase_changed, candidate_settled,
best_updated, budget and warning through the events socket; the local
implementation still only logs at debug.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
@zoroyihan7
zoroyihan7 requested a review from a team as a code owner October 23, 2026 09:50
@zoroyihan7 zoroyihan7 added the type:feature New capability or improvement label Oct 23, 2026
@github-actions

github-actions Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Code metrics gate: PASSED

Judged: the 18 files this change touches. Files it does not touch never fail it; anything found there is listed under Outside this change for information.

Dimension Limit New Worse Baseline out of date Baseline entries vs base
Cyclomatic complexity fails > 20 0 0 0 6 +0
Cognitive complexity fails > 30 0 0 0 53 +0
Function length (lines) fails > 50 0 0 0 545 +0
Nested block depth fails > 4 0 0 0 44 +0
Parameter count fails > 5 0 0 0 3 +0
Module length (lines) warns > 800, fails > 1200 0 0 0 43 +0
Duplicated lines per file fails > 0 0 0 0 95 +0
Dead code (vulture) fails > 0 0 0 0 2 +0
Directory size (entries) fails > 50 0 0 0 2 +0
Check Findings Effect
Comments 0
Broad catch without a reason 0
English only 0
Production code imports test code 0
Repeated literals 0
A1 Private name of another package 0
A8 Flag argument 0
A9 Tuple return 0
A10 Raw vocabulary comparison 0
A11 Hidden global write 0
A12 Pure forwarder 0
A12 Single-implementation abstraction 0
20 findings in files this change does not touch

Outside this change (informational): 20

Unit Dimension Baseline Now
src/hyperloom/orchestrator/actions/executors/_integrate_apply.py ApplyStageMixin._stage_apply Cognitive complexity absent 32
src/hyperloom/orchestrator/kernel/serving_log_evidence.py resolve_forge_server_log Cognitive complexity absent 49
src/hyperloom_kb/collect/mapping.py _experience Cognitive complexity absent 31
src/kernelforge/cli/forge_loop_command.py forge_loop Cognitive complexity absent 31
src/kernelforge/loop/runner.py IterationLoop._collect_session_candidate Cognitive complexity absent 44
src/kernelforge/loop/runner.py IterationLoop._rule_on_attempt Cognitive complexity absent 38
src/hyperloom/orchestrator/kernel/integrate_accuracy.py grade_integrate_accuracy Function length (lines) absent 56
src/hyperloom/orchestrator/loop/_resume_recovery.py ResumeRecoveryMixin._resume_recover_pending_warm_replay Function length (lines) absent 75
src/hyperloom_kb/http_service.py Module length (lines) absent 1525
src/kernelforge/cli/forge_loop_command.py Module length (lines) absent 2003
src/hyperloom/inference_optimizer/cli/preflight.py _probe_rocm_build Function length (lines) 54 57
src/kernelforge/gemm_tune/tuners/_aiter_dense_common.py run_aiter_dense_tuner Function length (lines) 118 119
src/hyperloom/inference_optimizer/breakdown/schema.py Module length (lines) 2681 2686
src/hyperloom/orchestrator/prompts/prompt_builder.py Module length (lines) 1205 1212
src/hyperloom/orchestrator/prompts/specialist_prompt_builder.py Module length (lines) 2556 2567
src/kernelforge/fusion/command.py Module length (lines) 1584 1708
src/kernelforge/gemm_tune/tuners/_aiter_dense_common.py Module length (lines) 1248 1249
src/kernelforge/loop/runner.py Module length (lines) 6051 6510
src/kernelforge/loop/task_preparer.py Module length (lines) 1299 1557
src/hyperloom/inference_optimizer/cli/__init__.py Module length (lines) 2727 2723

Verdict computed by the gate scripts in /home/runner/work/_temp/code-metrics-gate, not by this tree's copy.

Thresholds and sources: [tool.hyperloom.code_metrics] in pyproject.toml. Tools: complexipy 8.0.1, jscpd 5.4.1, ruff 0.16.2, vulture 2.16.

Comment thread src/hyperloom/hosted/events.py Fixed
Comment thread src/hyperloom/plugs/progress.py Fixed
Comment thread src/hyperloom/plugs/progress.py Fixed
Name repeated literals, keep the budget throttle on an object, lower the
writeback.py baseline to its new length, and create segments without world
read permission.

Co-Authored-By: Claude <noreply@anthropic.com>
Comment thread src/hyperloom/hosted/events.py Fixed
zoroyihan7 and others added 7 commits October 23, 2026 10:12
Co-Authored-By: Claude <noreply@anthropic.com>
writeback.py was split into per-job modules on main (#2132); re-apply the
winner_vetoed warning and best_updated emit in _current_best_lift.py.

Co-Authored-By: Claude <noreply@anthropic.com>
…eback test

Co-Authored-By: Claude <noreply@anthropic.com>
…tion

hosted-cpu no longer names an environment variable and 'events=hosted' now
resolves, so the unset-variable case uses a manifest and the not-installed case
is refused at the measurement socket.

Co-Authored-By: Claude <noreply@anthropic.com>

@chaojhou chaojhou left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

PR #2138 -- [hosting] Hosted progress events: the events plug implementation and its emit points

What it does: adds the hosted implementation of the events plug socket (src/hyperloom/hosted/events.py). It writes CloudEvents 1.0 JSON lines into rolling 4 MiB segment files under HostInfo.events_dir:

  • event ids rise strictly and continue after a restart;
  • a half-written last line is cut off;
  • fsync runs at most once a second.

It also adds a never-raising emit facade (src/hyperloom/plugs/progress.py). The legacy flow calls it at its existing write points: phase_changed, candidate_settled, best_updated, a budget event throttled to one per 5 minutes, and three warning events. The PR ships a v1 data schema, switches the hosted-cpu and hosted-gpu profiles to events=hosted, and documents the stream format.

Blocking issues: 2

  1. [free:roll-failure-wedges-stream] One failed os.open during a segment roll breaks the events writer for the rest of the run [verified]
    Problem: _roll_if_full (src/hyperloom/hosted/events.py:148-154) closes the current fd, sets self._fd = None and increments self._segment before it calls _open_segment(). If os.open raises there (EMFILE, ENOSPC, or EIO on the shared events dir), _fd stays None while _size is still above the cap. Every later emit() takes the roll branch again and calls os.close(None), which raises TypeError. emit() logs that each time, but nothing ever reopens a segment.
    Impact: after one transient filesystem error at a 4 MiB boundary, every later progress event in the run is lost. That includes phase_changed, best_updated, and the campaign_finished / campaign_failed events the hosted entry sends at the end. The platform tailing the directory sees the run stop reporting partway. This breaks the PR's own contract that a failed write is logged and the stream carries on; test_a_failed_write_is_logged_not_raised_and_leaves_no_partial_line pins that contract only for the write path.
    Action: open the next segment before giving up the current one, or increment _segment and replace _fd only after os.open succeeds, so the next emit retries the roll. Add a test in which os.open fails once during a roll and the following emit lands in the new segment with the next id.

  2. [X2] The description names the wrong file for best_updated and claims a writeback.py baseline change that is not in the diff [verified]
    Problem: the description says best_updated is emitted in orchestrator/loop/writeback.py _write_current_best. It also says writeback.py shrinks by one line, that its code-metrics baseline entry drops to 4155, and that two function-local imports were removed. At head 82f548f the emit is at src/hyperloom/orchestrator/loop/_current_best_lift.py:424, because #2132 moved _write_current_best there. The diff does not touch writeback.py or the baseline, and only one function-local import was removed (shared_state.py).
    Impact: the release is built from this description, and reviewers use it to find the emit sites. As written, it points to a file the PR does not change and to a baseline edit that does not exist.
    Action: name _current_best_lift.py _write_current_best (and _refuses_graded_winner for the winner_vetoed warning) as the emit sites. Remove the writeback.py / 4155 / two-imports sentence, or restate it as what the diff does.

The bot findings are addressed at this head: the segment files open with mode 0o600 (events.py:139), and the unused _budget_last global is gone. CI at 82f548f is green, and the branch merges cleanly with current main. Note that this implementation receives HostInfo through PlugContext.host_info, which open_plug_host does not yet supply on main; see the second finding on #2134.

Checked: hosted/events.py, plugs/progress.py, plugs/host.py, plugs/socket.py, plugs/events_local.py, hosted/host_info.py, the emit call sites (machine.py _commit_transition, _current_best_lift.py, shared_state.py), the schema, the env-var doc, the bot review threads | Ran: direct-call probes of the events writer on an exported head tree; git log -- writeback.py; git diff --stat base..head; git merge-tree --write-tree origin/main (clean) | Head: 82f548f

zoroyihan7 and others added 2 commits October 26, 2026 04:41
A failed os.open during a roll no longer leaves the writer without a descriptor; the next emit retries the roll.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: zoroyihan7 <Yihan.Wang@amd.com>
@zoroyihan7

Copy link
Copy Markdown
Collaborator Author

Addressed the CHANGES_REQUESTED review:

  • [free:roll-failure-wedges-stream] _roll_if_full now opens the next segment first and only then closes the old fd and replaces _fd/_segment, so a failed os.open leaves the writer on the current segment and the next emit retries. New test test_a_failed_segment_roll_is_retried_by_the_next_emit (os.open fails once during a roll; the next emit lands in the new segment with the next id). Reverse proof: with the old events.py it fails (['one'] != ['one','two']). Commit 6bcda9d.
  • [X2] Description corrected: emit site is _current_best_lift.py _write_current_best (and _refuses_graded_winner); the writeback.py / 4155 claim is removed.
  • Merged origin/main (merge commit 0a694fb), clean.

@zoroyihan7
zoroyihan7 requested a review from chaojhou October 26, 2026 04:50
The legacy flow does not call measurement, agent runner or GPU token through plug sockets; those belong to the multibranch hosted entry, which declares them in its own profile. A socket the profile leaves out is not started, and PlugHost.get raises if something asks for it.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: zoroyihan7 <Yihan.Wang@amd.com>
@zoroyihan7

Copy link
Copy Markdown
Collaborator Author

hosted-gpu profile now declares only events=hosted (commit fd87991). The legacy flow does not call measurement, agent runner or GPU token through plug sockets; those sockets belong to the multibranch hosted entry, which declares them in its own profile. A socket the profile omits is not started, and PlugHost.get raises if something asks for it. hosted-cpu is unchanged (it is the multibranch profile). Verified on a local, unpushed merge with #2134's head c9d8b62: the real hosted entry resolves the profile (sockets {events: hosted}), the hosted events implementation starts, and the first event is written to $HYPERLOOM_STATE_DIR/events/0000000001.jsonl. Note: #2134's _plug_lock refuses a profile that has no measurement socket, so #2134 needs a one-line follow-up (lock nothing when the profile has no measurement socket) before the two work together.

@chaojhou
chaojhou merged commit c291ff9 into main Oct 26, 2026
42 checks passed
@chaojhou
chaojhou deleted the hosting/h2-01-events branch October 26, 2026 06:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type:feature New capability or improvement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants