Skip to content

Spacedock stage strips are missed when boot output lands past the 512 KB head scan #221

Description

@iamcxa

Summary

A Spacedock first officer whose spacedock status --boot output lands more than 512 KB into its transcript gets no role badge and no stage strip. The session renders as an ordinary one and the ?next=true project panel reads This project declares no workflow.

transcript_boot() reads only the first spacedock_boot_scan_bytes (512,000) of the transcript. Its docstring states the assumption:

Boot output is written once at session start and never rewritten, so the scan is amortised

That holds when the FO agent is launched directly. It does not hold when a normal session adopts the first-officer role mid-conversation by loading the skill — boot then runs wherever the conversation happened to reach, and on a long session that is far past the window.

Verified against main at d4e3904 (v0.16.0).

Affected code

  • cargento/skills/cargento/cargento_runtime/spacedock.py
    • transcript_boot() — reads handle.read(config.spacedock_boot_scan_bytes) and nothing else. There is no tail read and no incremental resume, so an envelope past the window is unreachable for the life of the session.
    • boot_records() / workflow_dirs() — both correct; they simply never receive the bytes.
  • cargento/skills/cargento/cargento_runtime/config.py
    • spacedock_boot_scan_bytes=512_000 (line 468). No CLI flag and no environment override, so an operator who hits this has nothing to turn.

Deterministic reproduction

Two transcripts carrying the same valid boot envelope, differing only in where it sits:

import json, os, sys, tempfile, time
from pathlib import Path
from cargento_runtime import config as C, spacedock as S, state as runtime_state

cfg = C.build_runtime_config(environ=os.environ, platform_name=sys.platform,
                             os_name=os.name, launcher_path=Path("server.py").resolve())
st = runtime_state.RuntimeState(config=cfg, server_started=time.time())

def rec(t):
    return json.dumps({"type": "user", "message": {"role": "user",
        "content": [{"type": "tool_result", "tool_use_id": "t", "content": t}]}})

env = json.dumps({"command": "boot",
                  "definition_dir": "/tmp/wf/docs/dev",
                  "entity_dir": "/tmp/wf/docs/dev/.spacedock-state"})

deep    = ("\n".join([rec("x" * 20_000)] * 40 + [rec("boot:\n" + env)]) + "\n").encode()
shallow = (rec("boot:\n" + env) + "\n").encode()

with tempfile.TemporaryDirectory() as d:
    for name, blob in (("deep", deep), ("shallow", shallow)):
        p = os.path.join(d, name + ".jsonl")
        open(p, "wb").write(blob)
        r = S.transcript_boot(cfg, st, p)
        print(name, len(blob), len(r), S.workflow_dirs(cfg, r))

Actual:

deep    805006  0  []
shallow    246  1  ['/tmp/wf/docs/dev']

Expected: both find the envelope.

boot_records() on the same bytes confirms the parser is not at fault — it is purely the slice it is handed:

boot_records(cfg, blob[:512_000])  -> 0 envelopes
boot_records(cfg, blob)            -> 1 envelope

Observed in the wild

A real first-officer transcript on this machine, paths generalised:

transcript ~/.claude/projects/<encoded>/<session>.jsonl, 6,995,235 bytes
boot command ${SPACEDOCK_BIN:-spacedock} status --boot --identify --json, at record 1927 of 4412
earliest definition_dir byte offset 3,306,740 (4.7% → 47% of the file; 6.5× past the window)
transcript_boot() result []
boot_records() on the whole file 1 envelope, definition_dir = <workflow-repo>/docs/dev

So the data is well-formed and present; it is only out of reach.

Suggested fixes

Rough order of cost:

  1. Also scan the tail. Cheapest and it matches how the rest of the collector already treats transcripts (head + tail). Boot is never rewritten, so a head-plus-tail union has no correctness cost, but it still misses an envelope stranded in the middle of a very long session — as the one above is.
  2. Incremental whole-file scan, the way the turn scanner already does it (SKILL.md: "JSONL harnesses use an incremental whole-file scanner"). Reuses an existing pattern, is complete, and the existing (path, size) cache key already gives the resume point. Preferred if the scanner is reusable.
  3. Cheap prefilter, then targeted decode. definition_dir is a rare literal; one pass of bytes.find over the file to locate candidate offsets, then decode only those lines. Bounded cost on a multi-MB transcript without holding it all in memory.
  4. Expose the window (--spacedock-boot-scan-bytes, or an env var). Not a fix, but it would have let this be diagnosed and worked around locally instead of by reading the source.

Notes

  • Filing this as the scan-window bug alone. Two adjacent behaviours came up while tracking it down and are not claimed as defects here:
    • A mid-session FO has no agentSetting, so it gets no role badge even if the strip is fixed. That looks like intended behaviour per SKILL.md, but it means the two signals fail together for exactly this launch shape.
    • definition_dir can name a different repo than the session's cwd, so the strip would attach to the label of the FO's working directory rather than the workflow's. Worth a separate look at whether that is the intended attribution.
  • Nothing here needs the ask lane, usage fetch, or events; --no-spacedock is unrelated.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions