A finished turn ends its run without the prompt's replay, and a reported run the idle cap ends is done #131

Owner

Tracker: #3721

Step 1 of plan #3694. Fixes #3691, and takes the report rule of #3687.

What it delivers

  • A turn settles without the prompt's replay (dd25892). The pump counts only the messages handed to a run mid-run, and each adapter marks only their replays. The stream-json harness sometimes leaves out the prompt's replay: Planner run 358 (stream 7567) has no isReplay line in its 836 events, so its stdin never closed and the idle cap killed it. The stream-json adapter now takes the first replay for the prompt's only when it carries the prompt's text. The harness replays the prompt first or not at all, and the replay carries the text verbatim (checked on stream 7636). The app-server adapter takes the thread's first user-message item for the prompt's, and the ACP adapter the first prompt the agent names running. A turn that ends with no handed message outstanding now closes the harness's stdin, whether or not the prompt was replayed. A message handed mid-run still holds stdin open until its replay and the turn after it end.
  • A reported run the idle cap ends is done (78d3241). The pump reads SAK_REPORT once the harness has exited. A run the idle cap ends, whose report is a JSON object, ends done with it: the stream closes done with the report on its result, and the request is done. This holds whether its turn had ended or a tool call never returned, as in run 360 (#3693), whose tool call waited on a macOS privacy prompt. A run with no report, or one that is not a JSON object, still ends killed · idle_timeout. The wall cap is unchanged. The stream's close carries the same report text the rule read. tracker.report_of is the one test of "a JSON object" for both. The Outcome doc in runner/run.sx says so (aaac369).
  • A step is told to write its report last (234bb3e). STEP_REPORT_HOW now adds: "Write the report last, once your work is over, and wait in the foreground for long work, such as a gate, before your turn ends: your run ends when your turn does, and a run that falls silent once its report is written ends with that report."
  • A silent tail's heartbeat follows the clock (ec78bde). The flow's full gate step (job 75, on m5) failed this head only on tests/step_concurrency.sx "the slot a dropped tail left behind is handed on once its lease lapses". The cause is in sak's tail, not only in the machine. tail_produce sent its : tick comment every ten polls and counted each poll as POLL_MS. A poll is a 100 ms sleep plus three reads, and on a runner whose timers the system stretches, a 100 ms sleep takes 100–250 ms. So the heartbeat came every 2.3–2.4 s, not "about once a second" as docs/api.md says. A dropped tail renews its slot until its first heartbeat after the drop, so its lease lapsed after the test's reclaim at drop + 8 s. The heartbeat now goes out once HEARTBEAT_MS has passed on the clock since the tail last sent framing.
  • runner/README.md: One run 11 (the settle rule) and 14 (the idle-cap outcome), and the Harnesses section (which replay is the prompt's for each protocol). docs/api.md does not describe the close's status rules, so it is unchanged.

One case stays open. Suppose the stream-json harness skips the prompt's replay, and a handed message's text is exactly the prompt's. That message's replay is then taken for the prompt's, and the run holds stdin open until the idle cap. It ends done there if its report is written.

Flow run 36 (#3687) and master

git fetch origin master gave origin/master = 3ecd9d1 at 00:57 EEST on 2026-10-24, and 6e5e668 at 03:30 and 05:20. Neither held a merge of task/3687-a-run-whose-turn-ends-while-its-backgrou, so the commits above were built on 3ecd9d1 alone. At 07:17, origin/master was 4bd0747, the merge of #3687's PR 128 (07:13). So this branch merges origin/master (c61164d), and builds on #3687's rules without reimplementing them. That merge also brings the rest of master since 3ecd9d1 into this PR: PR 130's stream media (6e5e668). Master's fa0995c is the same tail fix as ec78bde, so src/api.sx merged clean.

Two files conflicted, and each kept both sides:

  • runner/run.sx. pump keeps master's signature, with report last. Master's line on a session's running tasks, written when a turn ends without the report while background tasks run, counts as a handed message: p.handed += 1. So the turn it opens settles once that line's replay arrives, whether or not the harness replayed the prompt. The stream-json adapter never takes that line's replay for the prompt's, because its text is not the prompt's. A run that a session's background tasks hold ends done through master's SIGTERM once a turn writes the report. A run the idle cap ends is done through this branch's report rule.
  • runner/README.md One run 11 is this branch's settle rule followed by master's background-task rule, which now says the runner's line counts "as a handed message is".

At 09:56 EEST origin/master was 13c9709, the merge of #3651's PR 129. It carries 3ee9597, which quotes the PATH tests/step9_release.sx hands install.sh (#3745), and the stream goldens that hold one width for each id and clock they hide. The branch merges it again (cbf1316), which brings PR 129 into this PR too. No runner file or runner test changes in that merge, so the regressions below stand at the head. The merge is clean: git diff-tree --cc cbf1316 prints only its hash, and git diff origin/master cbf1316 is byte-identical to git diff 4bd0747 c61164d. The plan's branch was still 3ecd9d1.

Regressions

New checks in tests/runner_core.sx, on two new modes of the stub stream-json harness:

  • check_unreplayed. The unreplayed 0 mode skips the prompt's replay, writes its report and ends its turn. Its run must end done with the report before the 5 s idle cap. The unreplayed 1 mode also gets a handed message, and the run must end done after that message's replay and the turn after it.
  • check_hung_report. The hung object|none|array mode replays the prompt and writes a JSON object, no report or a JSON array. It then prints a tool_use line with no result and stays silent past the 1 s idle cap.

By-hand sx run tests/runner_core.sx with the gate's pinned sx 300086027f (the gate cache, on m5 unless the row says s32):

checkout result the new runs in the runner's log
base 3ecd9d1 runner code, new tests and stub RUNNER CORE FAIL (3 checks), 382 ok unreplayed runs 5 and 6 killed · idle_timeout at the 5 s cap; hung-object run 7 killed · idle_timeout; runs 8 (none) and 9 (array) killed · idle_timeout
aaac369, before the merge RUNNER CORE OK, 385 ok runs 5 and 6 done in the second they started; run 7 done at the 1 s cap; runs 8 and 9 killed · idle_timeout
master 4bd0747 (#3687 merged), new tests and stub; on s32 RUNNER CORE FAIL (3 checks), 388 ok runs 5 and 6 killed · idle_timeout at the 5 s cap; runs 7, 8 and 9 killed · idle_timeout
head, the merge c61164d; on s32 RUNNER CORE OK, 391 ok runs 5, 6 and 7 done; runs 8 and 9 killed · idle_timeout; #3687's check_background_wait (run 13) and check_server_report (run 14) done

The three checks that fail at the base, and at master with #3687 merged:

FAIL a turn that ends with no message handed ends its run though the harness did not replay the prompt: it closes done with its report, long before the idle cap
FAIL the first replay of a run whose prompt the harness did not replay is the handed message's: the turn after it ends the run done, long before the idle cap
FAIL a run the idle cap ends in a tool call that never returned, its report a JSON object, closes done with that report, and its request completes done

The no-report and not-an-object checks pass at every arm, as they must. check_second_turn, check_driver, check_session_line, check_wakeup and the app-server and ACP steer and queue checks pass unchanged at the head.

A by-hand probe drops the stream-json adapter's text check, so its first replay is always taken for the prompt's. runner_core then reads RUNNER CORE FAIL (1 checks): only the handed-message check fails, its run ending done at the 5 s idle cap through the report rule.

The tail's heartbeat

Master carries the same fix, as fa0995c. The regression is the existing step_concurrency check that failed at the gate step. An instrumented copy of the tree printed each tail's ticks. At the pre-fix code, a dropped tail's last tick came 2.33 s after the drop, and the other tails' ticks were 2.34–2.44 s apart. With the fix, the last tick came 1.08 s after the drop, and ticks were 1.0–1.25 s apart.

By-hand sx run tests/step_concurrency.sx on s32 with the gate's pinned sx 300086027f, between 03:31 and 03:40 EEST, at a one-minute load of 2–3.4. Python's time.sleep(0.1) took 100–250 ms in the same shell.

code runs lease check
head 234bb3e (pre-fix tail, same as the base) 2 failed 2
base 3ecd9d1 5 failed 3
fix ec78bde 6 passed 6

The failing runs read:

ERROR: 2026-10-24T00:32:05Z GET /api/streams/7/events -> 503 22b 0ms every held slot is taken
FAIL the slot a dropped tail left behind is handed on once its lease lapses
CONCURRENCY FAIL (1 checks)

Base and fix runs alternated four times, so both arms saw the same machine. Base failed 3 of 4 and the fix passed 4 of 4. A deterministic fail-before would need a hook that slows a poll, and the tail has none. A check of the heartbeat's spacing would depend on timer speed in the same way.

Gates

Golden set: none.

At the head cbf1316, on s32:

  • Gate run 291 (sak-gate full #1, 10:23:58–10:51:56 EEST, clean tree) passed every step but the last:

    • both sx builds;
    • npm run build, with vitest at 401 of 401 in 5.56 s;
    • the libc grep;
    • every tests/*.sx file, runner_core.sx and step_concurrency.sx included;
    • scripts/release.sh, its own vitest again 401 of 401, and then tests/step9_release.sx, which now passes on s32 with the PATH quoting from master;
    • the deploy contract (PASS);
    • the main Playwright config: 740 passed, 1 skipped (4.9 min).

    It failed only at the runner Playwright config's global setup: sak serve exited with code 1, sak serve never answered on http://127.0.0.1:18988. That port is held by pid 6685, /private/tmp/sak-run-75-4b1062ec/work/dist/sak serve … --port 18988, which has been up 2 days 18 hours. Run 75 left it behind. It fails this step on s32 whatever the branch.

  • The runner Playwright config, by hand (10:52–10:53), from a git archive of cbf1316 in scratch. The arm used gate run 291's dist/sak, dist/sak-runner, dist/sak-runner-live and web/dist, plus the gate cache's web/node_modules and chromium. Its port was changed to 18978 in the arm only: 10 passed (35.5 s). The checkout was not touched.

The flow's gate step at c61164d, on s32: job 103 (09:54:29–09:55:53 EEST) failed in npm run build. Its vitest failed one of 401 tests: NavigatorRoute.test.ts "shows the counts of the loaded tracker" ran out its 1 s waitFor (1140 ms). Duration 16.97 s, environment 102.59 s. That is open task #3731. The branch's web/ equals master's.

Earlier heads:

  • c61164d, on s32: gate run 269 failed in vitest on the same #3731 test while job 92's sx gate ran beside it. Gate run 271 passed every step up to the release step, tests/*.sx included, then failed only tests/step9_release.sx's three install.sh checks (env: Support/JetBrains/Toolbox/scripts: No such file or directory, #3745). By hand, with the PATH's one spaced entry dropped: step9 STEP9 OK, the deploy contract PASS, the main Playwright config 740 passed and 1 skipped once rerun in a quiet window, and the runner Playwright config 10 of 10 on port 18978, because pid 6685 holds 18988 on s32.
  • aaac369, on m5: gate run 260 passed every step of full. Job 88, on s32, failed only at step9 (#3745).
  • ec78bde, on m5: gate run 259 passed every step of full. On s32, gate run 250 failed only at step9, and job 81 failed on #3731's timeouts in the release step's second vitest.
  • 234bb3e, on m5: gate runs 224 and 232 and job 75 failed, on two web timeouts and on step_concurrency.sx's lease check, which ec78bde fixes.
Tracker: [#3721](https://sak.swipelab.com/tasks/3721) Step 1 of plan [#3694](https://sak.swipelab.com/tasks/3694). Fixes [#3691](https://sak.swipelab.com/tasks/3691), and takes the report rule of [#3687](https://sak.swipelab.com/tasks/3687). ## What it delivers - **A turn settles without the prompt's replay** (dd25892). The pump counts only the messages handed to a run mid-run, and each adapter marks only their replays. The stream-json harness sometimes leaves out the prompt's replay: Planner run 358 (stream 7567) has no `isReplay` line in its 836 events, so its stdin never closed and the idle cap killed it. The stream-json adapter now takes the first replay for the prompt's only when it carries the prompt's text. The harness replays the prompt first or not at all, and the replay carries the text verbatim (checked on stream 7636). The app-server adapter takes the thread's first user-message item for the prompt's, and the ACP adapter the first prompt the agent names running. A turn that ends with no handed message outstanding now closes the harness's stdin, whether or not the prompt was replayed. A message handed mid-run still holds stdin open until its replay and the turn after it end. - **A reported run the idle cap ends is done** (78d3241). The pump reads `SAK_REPORT` once the harness has exited. A run the idle cap ends, whose report is a JSON object, ends `done` with it: the stream closes `done` with the report on its result, and the request is `done`. This holds whether its turn had ended or a tool call never returned, as in run 360 (#3693), whose tool call waited on a macOS privacy prompt. A run with no report, or one that is not a JSON object, still ends `killed · idle_timeout`. The wall cap is unchanged. The stream's close carries the same report text the rule read. `tracker.report_of` is the one test of "a JSON object" for both. The `Outcome` doc in `runner/run.sx` says so (aaac369). - **A step is told to write its report last** (234bb3e). `STEP_REPORT_HOW` now adds: "Write the report last, once your work is over, and wait in the foreground for long work, such as a gate, before your turn ends: your run ends when your turn does, and a run that falls silent once its report is written ends with that report." - **A silent tail's heartbeat follows the clock** (ec78bde). The flow's `full` gate step (job 75, on m5) failed this head only on `tests/step_concurrency.sx` "the slot a dropped tail left behind is handed on once its lease lapses". The cause is in sak's tail, not only in the machine. `tail_produce` sent its `: tick` comment every ten polls and counted each poll as `POLL_MS`. A poll is a 100 ms sleep plus three reads, and on a runner whose timers the system stretches, a 100 ms sleep takes 100–250 ms. So the heartbeat came every 2.3–2.4 s, not "about once a second" as `docs/api.md` says. A dropped tail renews its slot until its first heartbeat after the drop, so its lease lapsed after the test's reclaim at drop + 8 s. The heartbeat now goes out once `HEARTBEAT_MS` has passed on the clock since the tail last sent framing. - `runner/README.md`: One run 11 (the settle rule) and 14 (the idle-cap outcome), and the Harnesses section (which replay is the prompt's for each protocol). `docs/api.md` does not describe the close's status rules, so it is unchanged. One case stays open. Suppose the stream-json harness skips the prompt's replay, and a handed message's text is exactly the prompt's. That message's replay is then taken for the prompt's, and the run holds stdin open until the idle cap. It ends `done` there if its report is written. ## Flow run 36 (#3687) and master `git fetch origin master` gave `origin/master` = 3ecd9d1 at 00:57 EEST on 2026-10-24, and 6e5e668 at 03:30 and 05:20. Neither held a merge of `task/3687-a-run-whose-turn-ends-while-its-backgrou`, so the commits above were built on 3ecd9d1 alone. At 07:17, `origin/master` was 4bd0747, the merge of #3687's PR 128 (07:13). So this branch merges `origin/master` (c61164d), and builds on #3687's rules without reimplementing them. That merge also brings the rest of master since 3ecd9d1 into this PR: PR 130's stream media (6e5e668). Master's fa0995c is the same tail fix as ec78bde, so `src/api.sx` merged clean. Two files conflicted, and each kept both sides: - `runner/run.sx`. `pump` keeps master's signature, with `report` last. Master's line on a session's running tasks, written when a turn ends without the report while background tasks run, counts as a handed message: `p.handed += 1`. So the turn it opens settles once that line's replay arrives, whether or not the harness replayed the prompt. The stream-json adapter never takes that line's replay for the prompt's, because its text is not the prompt's. A run that a session's background tasks hold ends `done` through master's SIGTERM once a turn writes the report. A run the idle cap ends is `done` through this branch's report rule. - `runner/README.md` One run 11 is this branch's settle rule followed by master's background-task rule, which now says the runner's line counts "as a handed message is". At 09:56 EEST `origin/master` was 13c9709, the merge of #3651's PR 129. It carries 3ee9597, which quotes the PATH `tests/step9_release.sx` hands install.sh (#3745), and the stream goldens that hold one width for each id and clock they hide. The branch merges it again (cbf1316), which brings PR 129 into this PR too. No runner file or runner test changes in that merge, so the regressions below stand at the head. The merge is clean: `git diff-tree --cc cbf1316` prints only its hash, and `git diff origin/master cbf1316` is byte-identical to `git diff 4bd0747 c61164d`. The plan's branch was still 3ecd9d1. ## Regressions New checks in `tests/runner_core.sx`, on two new modes of the stub stream-json harness: - `check_unreplayed`. The `unreplayed 0` mode skips the prompt's replay, writes its report and ends its turn. Its run must end `done` with the report before the 5 s idle cap. The `unreplayed 1` mode also gets a handed message, and the run must end `done` after that message's replay and the turn after it. - `check_hung_report`. The `hung object|none|array` mode replays the prompt and writes a JSON object, no report or a JSON array. It then prints a `tool_use` line with no result and stays silent past the 1 s idle cap. By-hand `sx run tests/runner_core.sx` with the gate's pinned sx 300086027f (the gate cache, on m5 unless the row says s32): | checkout | result | the new runs in the runner's log | |---|---|---| | base 3ecd9d1 runner code, new tests and stub | `RUNNER CORE FAIL (3 checks)`, 382 ok | unreplayed runs 5 and 6 `killed · idle_timeout` at the 5 s cap; hung-object run 7 `killed · idle_timeout`; runs 8 (none) and 9 (array) `killed · idle_timeout` | | aaac369, before the merge | `RUNNER CORE OK`, 385 ok | runs 5 and 6 `done` in the second they started; run 7 `done` at the 1 s cap; runs 8 and 9 `killed · idle_timeout` | | master 4bd0747 (#3687 merged), new tests and stub; on s32 | `RUNNER CORE FAIL (3 checks)`, 388 ok | runs 5 and 6 `killed · idle_timeout` at the 5 s cap; runs 7, 8 and 9 `killed · idle_timeout` | | head, the merge c61164d; on s32 | `RUNNER CORE OK`, 391 ok | runs 5, 6 and 7 `done`; runs 8 and 9 `killed · idle_timeout`; #3687's `check_background_wait` (run 13) and `check_server_report` (run 14) `done` | The three checks that fail at the base, and at master with #3687 merged: ``` FAIL a turn that ends with no message handed ends its run though the harness did not replay the prompt: it closes done with its report, long before the idle cap FAIL the first replay of a run whose prompt the harness did not replay is the handed message's: the turn after it ends the run done, long before the idle cap FAIL a run the idle cap ends in a tool call that never returned, its report a JSON object, closes done with that report, and its request completes done ``` The no-report and not-an-object checks pass at every arm, as they must. `check_second_turn`, `check_driver`, `check_session_line`, `check_wakeup` and the app-server and ACP steer and queue checks pass unchanged at the head. A by-hand probe drops the stream-json adapter's text check, so its first replay is always taken for the prompt's. `runner_core` then reads `RUNNER CORE FAIL (1 checks)`: only the handed-message check fails, its run ending `done` at the 5 s idle cap through the report rule. ### The tail's heartbeat Master carries the same fix, as fa0995c. The regression is the existing `step_concurrency` check that failed at the gate step. An instrumented copy of the tree printed each tail's ticks. At the pre-fix code, a dropped tail's last tick came 2.33 s after the drop, and the other tails' ticks were 2.34–2.44 s apart. With the fix, the last tick came 1.08 s after the drop, and ticks were 1.0–1.25 s apart. By-hand `sx run tests/step_concurrency.sx` on s32 with the gate's pinned sx 300086027f, between 03:31 and 03:40 EEST, at a one-minute load of 2–3.4. Python's `time.sleep(0.1)` took 100–250 ms in the same shell. | code | runs | lease check | |---|---|---| | head 234bb3e (pre-fix tail, same as the base) | 2 | failed 2 | | base 3ecd9d1 | 5 | failed 3 | | fix ec78bde | 6 | passed 6 | The failing runs read: ``` ERROR: 2026-10-24T00:32:05Z GET /api/streams/7/events -> 503 22b 0ms every held slot is taken FAIL the slot a dropped tail left behind is handed on once its lease lapses CONCURRENCY FAIL (1 checks) ``` Base and fix runs alternated four times, so both arms saw the same machine. Base failed 3 of 4 and the fix passed 4 of 4. A deterministic fail-before would need a hook that slows a poll, and the tail has none. A check of the heartbeat's spacing would depend on timer speed in the same way. ## Gates Golden set: none. **At the head cbf1316, on s32:** - **Gate run 291** (`sak-gate full` #1, 10:23:58–10:51:56 EEST, clean tree) passed every step but the last: - both sx builds; - `npm run build`, with vitest at 401 of 401 in 5.56 s; - the libc grep; - every `tests/*.sx` file, `runner_core.sx` and `step_concurrency.sx` included; - `scripts/release.sh`, its own vitest again 401 of 401, and then `tests/step9_release.sx`, which now passes on s32 with the PATH quoting from master; - the deploy contract (`PASS`); - the main Playwright config: 740 passed, 1 skipped (4.9 min). It **failed** only at the runner Playwright config's global setup: `sak serve exited with code 1`, `sak serve never answered on http://127.0.0.1:18988`. That port is held by pid 6685, `/private/tmp/sak-run-75-4b1062ec/work/dist/sak serve … --port 18988`, which has been up 2 days 18 hours. Run 75 left it behind. It fails this step on s32 whatever the branch. - **The runner Playwright config, by hand** (10:52–10:53), from a `git archive` of cbf1316 in scratch. The arm used gate run 291's `dist/sak`, `dist/sak-runner`, `dist/sak-runner-live` and `web/dist`, plus the gate cache's `web/node_modules` and chromium. Its port was changed to 18978 in the arm only: **10 passed** (35.5 s). The checkout was not touched. **The flow's gate step at c61164d, on s32:** **job 103** (09:54:29–09:55:53 EEST) **failed** in `npm run build`. Its vitest failed one of 401 tests: `NavigatorRoute.test.ts` "shows the counts of the loaded tracker" ran out its 1 s `waitFor` (1140 ms). Duration 16.97 s, environment 102.59 s. That is open task #3731. The branch's `web/` equals master's. **Earlier heads:** - c61164d, on s32: gate run 269 failed in vitest on the same #3731 test while job 92's sx gate ran beside it. Gate run 271 passed every step up to the release step, `tests/*.sx` included, then failed only `tests/step9_release.sx`'s three install.sh checks (`env: Support/JetBrains/Toolbox/scripts: No such file or directory`, #3745). By hand, with the PATH's one spaced entry dropped: step9 `STEP9 OK`, the deploy contract `PASS`, the main Playwright config 740 passed and 1 skipped once rerun in a quiet window, and the runner Playwright config 10 of 10 on port 18978, because pid 6685 holds 18988 on s32. - aaac369, on m5: gate run 260 passed every step of `full`. Job 88, on s32, failed only at step9 (#3745). - ec78bde, on m5: gate run 259 passed every step of `full`. On s32, gate run 250 failed only at step9, and job 81 failed on #3731's timeouts in the release step's second vitest. - 234bb3e, on m5: gate runs 224 and 232 and job 75 failed, on two web timeouts and on `step_concurrency.sx`'s lease check, which ec78bde fixes.
The pump counts only the messages handed to a run mid-run, and each
adapter marks only their replays. The stream-json harness at times leaves
out the prompt's replay: Planner run 358 (stream 7567) ended its only turn
with no replay line, its stdin never closed, and the idle cap killed it.
Its adapter now takes the first replay for the prompt's only when it
carries the prompt's text. The app-server and ACP adapters take their
first replay, the thread's first user-message item and the first prompt
named running, for the prompt's.

A turn that ends with no handed message outstanding closes the harness's
stdin whether or not the prompt was replayed; a message handed mid-run
still holds it open until its replay and the turn after it end.
A run the idle cap ends, whose SAK_REPORT file holds a JSON object, has
concluded: it ends done with that report on its stream's result, and its
request done, whether its turn had ended or a tool call never returned.
Run 360 (#3693) is the second shape: its tool call waited on a macOS
privacy prompt nobody saw until the idle cap ended it. A run with no
report, or one that is not a JSON object, still ends killed with
idle_timeout, and the wall cap is unchanged.

The pump reads the report once the harness has exited, and the stream's
close carries that same text.
STEP_REPORT_HOW tells a step's agent to write its report last, once its
work is over, and to wait in the foreground for long work such as a gate
before its turn ends: the run ends when the turn does, and a run that
falls silent once its report is written ends with that report.
A tail sent its `: tick` comment every ten polls, counting each poll as
POLL_MS. A poll is a sleep plus three reads, and on a runner whose timers
the system stretches a 100 ms sleep takes 215-250 ms, so the heartbeat
came every 2.3-2.4 s. A dropped tail's slot is renewed until its next
heartbeat write faults, so its lease lapsed later than one heartbeat plus
the lease, and step_concurrency's reclaim check failed on both runners.

The heartbeat now goes out once HEARTBEAT_MS has passed on the clock
since the tail last sent framing, as docs/api.md describes it.
agra added 11 commits 2026-10-24 08:35:41 +03:00
A content block that carries a file through its source becomes a Media part of
the call's entry: its type, media type, a data URL of the bytes it holds or the
URL it names, and its size. The call's result keeps only the text, so the
stream holds no base64 as text. The line a stream-json run adds after an image
it resized folds into the call that read it and records the image's original
size.

Tracker: https://sak.swipelab.com/tasks/3683
A: a Read of an image in the stream, from stream 7394's own lines; B: that
image opened full size; C: a Read of a PDF. Each sheet shows every variant
light and dark at 1280x800 and 390x844, rendered in the app from patched
builds; none is built yet. The sheets leave the tree once the picks are built.

Tracker: https://sak.swipelab.com/tasks/3683
Viewer holds what the task gallery shows of any item: the dialog, its bar with
Zoom, Download and Close, the stage between Previous and Next, the strip and
the keys. ZoomImage is the zoomable image on its stage. The task gallery keeps
its tags, Delete and key sheet in the frame's snippets, and shows the same.

Tracker: https://sak.swipelab.com/tasks/3683
An image a call answered with shows under its row, fit in 11rem and framed,
with its type, size and bytes beneath. It opens in the gallery's viewer, which
pages through every image of the stream with zoom, the arrows and the strip,
and saves the shown one under its call's name. A document a call read is a row
of its type, name and bytes whose Open shows it in a tab of its own, from a
blob of its bytes.

Goldens: stream 7394 seq 103-125 and a Read of a PDF among its lines, light and
dark at 1280x800 and 390x844.

Tracker: https://sak.swipelab.com/tasks/3683
A url source of another scheme holds no src, so neither Download nor Open
can run it in the page. Open makes a blob only of a PDF, plain text or
raster image; a document of any other type is a row with no Open, since a
blob tab runs with sak's origin.
The stream-json adapter keeps the description of each background task the
session lists running. The first turn to end with no message pending, a task
listed and $SAK_REPORT unwritten gets one user line from the runner, counted
as a message is, with no ack: it names the running tasks and says that the
run stays up while they run and that the turn that writes the report ends the
run and every task still running, such as a server. The next turn's end closes
the session's stdin as before, so an agent that ends its turn with only a
server running is asked for its report instead of being held to the wall cap.
A tail sent its `: tick` comment every ten polls, counting each poll as
POLL_MS. A poll is a sleep plus three reads, and on a runner whose timers
the system stretches a 100 ms sleep takes 215-250 ms, so the heartbeat
came every 2.3-2.4 s. A dropped tail's slot is renewed until its next
heartbeat write faults, so its lease lapsed later than one heartbeat plus
the lease, and step_concurrency's reclaim check failed on both runners.

The heartbeat now goes out once HEARTBEAT_MS has passed on the clock
since the tail last sent framing, as docs/api.md describes it.
The runner's line on a session's running tasks counts as a handed message,
so the turn it opens settles once its replay arrives, whether or not the
harness replayed the prompt.
The stream specs' freeze hid ids and clocks with visibility: hidden, so
each still took its own width. A new stream id after a worker restart
moved the state chip by a fraction of a pixel, and a run time that
reached two digits on a loaded host moved Session and Context. freeze
now lives in stream-fixture.ts and sets each hidden value to one
reading first: a four-digit id, "just now", and "ran 0s" or
"running 0s". The finalized-metadata scene also pins its thinking
seconds, which count the time between two of the test's requests.

Regenerates the 15 stream page goldens those widths moved, in one full
run of playwright.config.ts.

Tracker: https://sak.swipelab.com/tasks/3651
tests/step9_release.sx ran install.sh through `env ... PATH={}/stubs:$PATH
sh -c ...` with the PATH unquoted. A run PATH that holds a directory with
a space split that assignment, and env ran the rest as a command, so
three install.sh checks failed. s32's run PATH holds "Application
Support", so the full gate failed there for every branch.

The assignment is quoted, and the launchctl stub's directory is named
with a space, so the check hands install.sh a spaced PATH entry on every
host.

Tracker: https://sak.swipelab.com/tasks/3745
agra merged commit 762efb1697 into task/3694-plan-a-flow-run-waits-on-agra-only-for-a 2026-10-24 12:46:12 +03:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
sx/sak!131
No description provided.