Skip to content

fix(bin): pass large fleet snapshot JSON to jq via files instead of argv - #2737

Open
Omar-Nawaf wants to merge 2 commits into
kunchenguid:mainfrom
Omar-Nawaf:fm/firstmate-fleet-snapshot-upstream-pr
Open

fix(bin): pass large fleet snapshot JSON to jq via files instead of argv#2737
Omar-Nawaf wants to merge 2 commits into
kunchenguid:mainfrom
Omar-Nawaf:fm/firstmate-fleet-snapshot-upstream-pr

Conversation

@Omar-Nawaf

Copy link
Copy Markdown

Intent

Publish the confirmed large-fleet snapshot correction in one upstream PR referencing #2629 (not duplicate #2613). Ensure every bin/fm-fleet-snapshot.sh call site that passes unbounded backlog or task aggregates to jq avoids Linux argv-size limits, while leaving bounded scalar --argjson uses unchanged unless evidence requires otherwise. Use the script's existing trap-cleaned temporary-file discipline or an equally direct single-owner input path, preserving jq semantics and normal-sized output. Preserve or improve an executable oversized regression where an ordinary source backlog comfortably beyond Linux MAX_ARG_STRLEN succeeds, and prove normal output compatibility without source-text assertions. Validate focused fleet snapshot, fleet view, and bearings behavior, the oversized regression, documentation audience checks if prose changes, exact bin/fm-lint.sh, and the full no-mistakes pipeline. Do not include private data, state, config, .env, project content, credentials, local paths, unrelated changes, or agent co-authorship. Push only this task branch through no-mistakes to the authorized public fork Omar-Nawaf/firstmate, open the PR against kunchenguid/firstmate:main, and never merge.

What Changed

  • bin/fm-fleet-snapshot.sh now writes unbounded JSON values (backlog, tasks, scout reports, main inventory, accepted child summaries, and the secondmate current/landed aggregates) to a trap-cleaned temporary directory and binds them in jq with --slurpfile, replacing the --argjson argv handoffs that failed with "Argument list too long" once a value exceeded the Linux per-argument cap (MAX_ARG_STRLEN, 131072 bytes); per-record secondmate roll-up also accumulates into a JSONL file instead of re-feeding a growing shell variable through jq argv. Bounded scalar --argjson uses are unchanged.
  • tests/fm-fleet-snapshot-view.test.sh adds three executable oversized regressions: a 400-item backlog beyond the argv cap in both --json and --secondmate-home-summary modes, a single child-home summary landing between the argv cap and the byte-acceptance guard, and a multi-home fleet aggregate that only crosses the cap when combined — each asserting normal, complete snapshot output without source-text assertions.
  • tests/fm-bearings-snapshot.test.sh adds mktemp and rm to the restricted toolbin in the perl-fallback bounds test so the snapshot script's new temp-file staging works under that fixture.

Risk Assessment

✅ Low: The fix-round commit is a semantics-preserving mechanical migration of every unbounded jq handoff to the trap-cleaned file/--slurpfile path, verified against each call site and covered by behavioral regressions that pin both the individual-summary (131073–262144 byte) and fleet-aggregate argv windows and would fail at the pre-fix head, with all intent constraints (bounded scalar argv preserved, jq bodies unchanged, no co-authorship or private data) confirmed and only one contrived informational failure-mode note remaining.

Testing

Ran the two focused suites touched by the change (fleet snapshot/view with its three new oversized regressions, and the full bearings snapshot suite) — all pass; manually proved the regression is real by running the base-commit script against a backlog whose parsed JSON exceeds Linux MAX_ARG_STRLEN (it fails with "Argument list too long") and the fixed script on the same fixture (complete 400-record snapshot); confirmed byte-identical output on normal-sized fixtures in both output modes, verified trap-based temp-directory cleanup leaves nothing behind, and exercised the real end-user surfaces (fm-fleet-view.sh and fm-bearings-snapshot.sh) over the oversized fleet. No visual artifact applies — this is a CLI/JSON tool, so CLI transcripts are the reviewer-visible surface. Lint, docs checks (no prose changed), and the full pipeline belong to other phases of this run.

Evidence: Before/after: base script fails with 'Argument list too long', fixed script snapshots 400 records

Source: Before/after: base script fails with 'Argument list too long', fixed script snapshots 400 records

$ # BEFORE (base commit a0cec26, --argjson passes backlog JSON on jq argv) $ FM_HOME=<fixture: 400-item queued backlog, parsed JSON > 131072 bytes> fm-fleet-snapshot.sh --json exit code: 1 --- stderr --- .../fm-fleet-snapshot.sh: line 611: jq: Argument list too long fm-fleet-snapshot: main inventory summary failed $ # AFTER (target commit 1954b6a, --slurpfile stages the same JSON through trap-cleaned temp files) exit code: 0 --- output checks --- backlog JSON bytes inside snapshot: 369977 (argv cap is 131072) backlog records: 400 main_inventory.valid: true

$ # BEFORE (base commit a0cec26, --argjson passes backlog JSON on jq argv)
$ FM_HOME=<fixture: 400-item queued backlog, parsed JSON > 131072 bytes> fm-fleet-snapshot.sh --json
exit code: 1
--- stderr ---
  /tmp/fm-argv-repro.ff6iHj/base-bin/fm-fleet-snapshot.sh: line 611: /home/omarn/.local/bin/jq: Argument list too long
  fm-fleet-snapshot: main inventory summary failed

$ # AFTER (target commit 1954b6a, --slurpfile stages the same JSON through trap-cleaned temp files)
$ FM_HOME=<same fixture> fm-fleet-snapshot.sh --json
exit code: 0
--- stderr ---
--- output checks ---
  backlog JSON bytes inside snapshot: 369977 (argv cap is 131072)
  backlog records:  400
  main_inventory.valid: true
Evidence: Normal-sized output byte-identical across base and fixed, temp staging trap-cleaned

Source: Normal-sized output byte-identical across base and fixed, temp staging trap-cleaned

mode --json: outputs byte-identical (5180 bytes) mode --secondmate-home-summary: outputs byte-identical (1453 bytes) # Temp-file cleanup: TMPDIR contents after fixed-script runs (should be empty) (empty — trap cleanup removed all fm-fleet-snapshot.* staging dirs)

# Normal-sized fixture: byte-for-byte output comparison, base vs fixed (FM_SNAPSHOT_NOW pinned)
mode --json: outputs byte-identical (5180 bytes)
mode --secondmate-home-summary: outputs byte-identical (1453 bytes)

# Temp-file cleanup: TMPDIR contents after fixed-script runs (should be empty)
  (empty — trap cleanup removed all fm-fleet-snapshot.* staging dirs)
Evidence: Fleet view and bearings render the oversized 400-item fleet (end-user CLI surfaces)

Source: Fleet view and bearings render the oversized 400-item fleet (end-user CLI surfaces)

$ # End-user surfaces over the same oversized 400-item backlog home (fixed build)
$ fm-fleet-view.sh   (human fleet render)
exit code: 0
--- first 20 lines ---
  # Fleet View
  
  Schema: fm-fleet-snapshot.v1
  Home: /tmp/fm-argv-repro.ff6iHj/home
  
  ## Under Way
  No live task metadata found.
  
  ## Queued
  | ID | Title | Repo | Kind | Blocked By | Artifact |
  | --- | --- | --- | --- | --- | --- |
  | hold-001 | Captain decision 001 | alpha | captain | - | - |
  | hold-002 | Captain decision 002 | alpha | captain | - | - |
  | hold-003 | Captain decision 003 | alpha | captain | - | - |
  | hold-004 | Captain decision 004 | alpha | captain | - | - |
  | hold-005 | Captain decision 005 | alpha | captain | - | - |
  | hold-006 | Captain decision 006 | alpha | captain | - | - |
  | hold-007 | Captain decision 007 | alpha | captain | - | - |
  | hold-008 | Captain decision 008 | alpha | captain | - | - |
  | hold-009 | Captain decision 009 | alpha | captain | - | - |
  ... (417 lines total)

$ fm-bearings-snapshot.sh   (bearings projection over the fleet snapshot)
exit code: 0
--- first 15 lines ---
  schema: fm-bearings.v1
  home: fm-argv-repro.ff6iHj/home
  generated: "2026-08-21T07:32:44Z"
  prs: "not_requested (run: /bearings include PRs)"
  in_flight: []
  secondmates: []
  decisions_open[20]{id,key,verb,summary,owner}:
    hold-001,hold-001,captain-hold,"Captain decision 001: captain must choose between the long-standing option A and the equal…",(main)
    hold-002,hold-002,captain-hold,"Captain decision 002: captain must choose between the long-standing option A and the equal…",(main)
    hold-003,hold-003,captain-hold,"Captain decision 003: captain must choose between the long-standing option A and the equal…",(main)
    hold-004,hold-004,captain-hold,"Captain decision 004: captain must choose between the long-standing option A and the equal…",(main)
    hold-005,hold-005,captain-hold,"Captain decision 005: captain must choose between the long-standing option A and the equal…",(main)
    hold-006,hold-006,captain-hold,"Captain decision 006: captain must choose between the long-standing option A and the equal…",(main)
    hold-007,hold-007,captain-hold,"Captain decision 007: captain must choose between the long-standing option A and the equal…",(main)
    hold-008,hold-008,captain-hold,"Captain decision 008: captain must choose between the long-standing option A and the equal…",(main)
  ... (40 lines total)
Evidence: Full fm-fleet-snapshot-view.test.sh transcript (18/18 ok, includes the three oversized regressions)

Source: Full fm-fleet-snapshot-view.test.sh transcript (18/18 ok, includes the three oversized regressions)

ok - empty fleet snapshot and view use explicit absence markers
ok - fixture snapshot covers task rows, backlog rows, pointers, and stable ordering
ok - a backlog larger than the argv cap still snapshots in both output modes
ok - a child summary between the argv cap and the byte guard still snapshots
ok - a fleet aggregate larger than the argv cap still snapshots
ok - main_inventory discloses orphan/unstructured and clears when inventory is consistent
ok - backlog normalization preserves strict roles and resolves every blocker compatibly
ok - snapshot event hints follow reconciled current state
ok - durable fold keeps an open decision past a later unrelated event
ok - a live secondmate endpoint preserves unrelated open decisions
ok - durable captain-held transfer closes the duplicate live status decision
ok - durable fold clears a decision only on a keyed resolution
ok - a completed scout's stale decision surfaces as a report pointer, not pending
ok - a scout still parked at a decision stays pending (terminal clear does not over-fire)
ok - snapshot includes durable scout reports after teardown
ok - snapshot parses tasks-axi rows and respects operational overrides
ok - fleet view renders the snapshot without secondmate peek guidance
ok - fleet view renders secondmate agent liveness

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 1 info
  • ⚠️ bin/fm-fleet-snapshot.sh:1292 - Residual argv-size window in the cross-home aggregation path: the intent requires that "every bin/fm-fleet-snapshot.sh call site that passes unbounded backlog or task aggregates to jq avoids Linux argv-size limits", but a child-home summary is still passed via --argjson summary (line 1292) after a byte guard whose default acceptance bound (FM_SNAPSHOT_SECONDMATE_MAX_BYTES=262144, line 82) is twice MAX_ARG_STRLEN (131072). A summary in the 131073–262144 byte window passes the check at line 1247 and then reproduces the exact fixed failure ("Argument list too long") in the parent. Concrete reachable paths: (a) the documented FM_SNAPSHOT_SECONDMATE_LANDED_PER_HOME=0 cap-lift (line 794 emits the child's full Done-section projection, i.e. an unbounded backlog aggregate); (b) the uncapped invalidity.ids/reason fields that scale with child task counts. Additionally, SECONDMATE_CURRENT_JSON (up to FM_SNAPSHOT_SECONDMATES=20 records, each embedding a summary that individually fit argv) can sum past the cap at the --argjson current (line 1348) and --argjson secondmate_current (line 1413) handoffs in a large busy fleet — the same population issue fm-fleet-snapshot dies at ~53 backlog items: unbounded aggregates passed to jq via argv hit the 128 KiB MAX_ARG_STRLEN cap #2629 concerns. These call sites are pre-existing and were deliberately left unchanged per the intent's "leaving bounded scalar --argjson uses unchanged" scoping, so this is flagged for the author's decision rather than as a blocker. Recommended shared boundary if pursued: route these aggregate handoffs through the jq_value_file/--slurpfile discipline this change introduces, or clamp FM_SNAPSHOT_SECONDMATE_MAX_BYTES below MAX_ARG_STRLEN.

🔧 Fix: stage child summaries and fleet aggregates to jq via slurpfile
1 info still open:

  • ℹ️ bin/fm-fleet-snapshot.sh:1337 - Failure-mode softening in the rewritten record accumulation: previously a failed per-record jq invocation inside secondmate_current_json cascaded into a loud snapshot failure (the next --argjson records &#34;$records&#34; accumulation errored and the script exited 1); now printf &#39;%s\n&#39; &#34;$record&#34; &gt;&gt;&#34;$records_file&#34; appends an empty line when $record is empty, --slurpfile silently skips the whitespace, and the mate's record is silently omitted from secondmate_current.records while shown/total (computed from the union before the loop) still count it, producing an internally inconsistent snapshot instead of an error. Reachability is contrived — it requires one of the loop's internal jq execs to fail (OOM, or JSON-escape inflation pushing a byte-bounded --argjson capture such as activities past MAX_ARG_STRLEN on a control-character-dense status log) — and that same trigger also failed pre-change, just loudly. If desired, a one-line guard ([ -n &#34;$record&#34; ] || return 1 before the append) restores fail-loud behavior; not required for merge.
✅ **Test** - passed

✅ No issues found.

  • bash tests/fm-fleet-snapshot-view.test.sh — all 18 tests pass, including the three new oversized regressions (backlog over the argv cap in both output modes, child summary between the argv cap and the byte guard, fleet aggregate over the cap)
  • bash tests/fm-bearings-snapshot.test.sh — fail-fast suite exited 0 (all 82 tests), including the modified test_perl_fallback_bounds_github_call whose minimal toolbin now carries mktemp/rm
  • Fail-before/pass-after manual repro: ran the base-commit bin/fm-fleet-snapshot.sh --json against a 400-item backlog home (parsed backlog JSON 369,977 bytes > MAX_ARG_STRLEN 131,072) — it fails with jq: Argument list too long; the target-commit script succeeds with all 400 records and main_inventory.valid == true
  • Output-compatibility check: base vs fixed script on a normal-sized fixture with FM_SNAPSHOT_NOW pinned — --json (5,180 bytes) and --secondmate-home-summary (1,453 bytes) outputs are byte-identical
  • Temp-file discipline: after fixed-script runs with a scratch TMPDIR, the directory is empty — the EXIT trap removed every fm-fleet-snapshot.* staging dir
  • End-user surfaces over the oversized home: bin/fm-fleet-view.sh renders the full 400-row fleet view (exit 0) and bin/fm-bearings-snapshot.sh produces its bearings projection (exit 0)
  • Checked both commit messages for forbidden content — no agent co-authorship, private data, or local paths
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

Omar-Nawaf and others added 2 commits August 21, 2026 09:46
A grown backlog's parsed JSON exceeded the Linux per-argument cap
(MAX_ARG_STRLEN, 131072 bytes), so every jq exec receiving it as a single
--argjson value failed with "Argument list too long" and the whole fleet
snapshot died at the main-inventory step.

Stage the backlog and task inventories in a trap-cleaned temp directory and
bind them with --slurpfile at each jq call site that consumes them. The jq
program bodies are unchanged, so output stays byte-identical for a
normal-sized backlog. Signal exits route through the same cleanup so a
timed-out cross-home read leaves no directory behind.

Add a colocated regression covering both output modes with a backlog JSON
larger than the cap, and give the Perl-timeout fixture's minimal toolbin the
mktemp/rm it now needs.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1954b6a8fe

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread bin/fm-fleet-snapshot.sh
registry=$(registry_secondmates_json) || return 1
union=$(jq -n --argjson registry "$registry" --argjson tasks "$tasks" '
($registry.records // []) as $registered
union=$(jq -n --argjson registry "$registry" --slurpfile tasks_in "$tasks_file" '

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Stage the registry JSON before invoking jq

Captain, when FM_SNAPSHOT_REGISTRY_BYTES and FM_SNAPSHOT_REGISTRY_RECORDS are raised for a large secondmate registry, $registry can exceed Linux's 131072-byte per-argument limit and this remaining --argjson invocation fails with Argument list too long, aborting the entire snapshot. I reproduced this with an approximately 208 KB registry and otherwise empty fleet; stage the registry in the temporary directory and load it via --slurpfile like the task data.

Useful? React with 👍 / 👎.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant