Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions bin/fm-line-cap-lib.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# shellcheck shell=bash
# Shared per-line cap for agent-facing digest lines.
# Usage: . bin/fm-line-cap-lib.sh; fm_cap_line "<line>" [<max>]
#
# ONE OWNER for the bounded-line shape both digests use. The wake digest's
# OPEN DECISIONS section (bin/fm-wake-drain.sh) and the session-start digest's
# per-task status tails (bin/fm-session-start.sh) render the same kind of
# content - an agent-written status line, which AGENTS.md section 8 treats as a
# wake EVENT rather than current state - into a size-bounded view. An agent
# reading both must recognize one truncation marker, and the two caps must not
# drift apart, so the cut and its marker live here.
#
# Callers keep their own composite policy: fm-wake-drain.sh still owns the
# OPEN DECISIONS global byte cap and its "N more omitted" disclosure, and
# fm-session-start.sh still owns how many tail lines it prints per task. This
# file owns only the per-line cut.
#
# The cap counts characters, so a plain-ASCII line - what status lines are in
# practice - is bounded to the same number of bytes, and a multibyte character
# is never cut in half into an invalid sequence.
# Truncation stays recoverable because the session-start digest prints each
# task's full status log path, while every OPEN DECISIONS entry begins with the
# task id that identifies its durable state/<id>.status source.

FM_LINE_CAP_DEFAULT=220
FM_LINE_CAP_SUFFIX=' [truncated]'

# fm_cap_line_var <line> [<max>]: put <line> in FM_LINE_CAP_LINE, cut to <max>
# characters with FM_LINE_CAP_SUFFIX in place of the tail when it is longer. A
# line at or under the cap is kept unchanged, marker and all bytes intact.
# This is the rule itself. It assigns rather than prints so a caller that needs
# the value - the wake digest builds its section in a variable to weigh each
# item against a global budget - never pays a command substitution per item on
# a path that runs at the top of every wake-handling turn.
fm_cap_line_var() {
local line=$1 max=${2:-$FM_LINE_CAP_DEFAULT} keep
if [ "${#line}" -le "$max" ]; then
FM_LINE_CAP_LINE=$line
return 0
fi
keep=$((max - ${#FM_LINE_CAP_SUFFIX}))
[ "$keep" -ge 0 ] || keep=0
FM_LINE_CAP_LINE="${line:0:$keep}$FM_LINE_CAP_SUFFIX"
}

# fm_cap_line <line> [<max>]: the same cut, printed on stdout, for a caller that
# is streaming lines rather than accumulating them.
fm_cap_line() {
fm_cap_line_var "$@"
printf '%s\n' "$FM_LINE_CAP_LINE"
}
254 changes: 196 additions & 58 deletions bin/fm-session-start.sh

Large diffs are not rendered by default.

14 changes: 8 additions & 6 deletions bin/fm-wake-drain.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
. "$SCRIPT_DIR/fm-wake-lib.sh"
# shellcheck source=bin/fm-classify-lib.sh
. "$SCRIPT_DIR/fm-classify-lib.sh"
# shellcheck source=bin/fm-line-cap-lib.sh
. "$SCRIPT_DIR/fm-line-cap-lib.sh"

DRAIN_TMP=
DRAIN_LOCK_HELD=false
Expand Down Expand Up @@ -43,7 +45,7 @@ assert_watcher_liveness() {
# common case.
print_open_decisions_section() {
local open task key verb note line item_bytes=220 global_bytes=4000
local output='' used=0 shown=0 omitted=0 bytes suffix keep
local output='' used=0 shown=0 omitted=0 bytes

open=$(scan_open_decisions_incremental "$STATE") || return 0
[ -n "$open" ] || return 0
Expand All @@ -53,11 +55,11 @@ print_open_decisions_section() {
line="$task"
[ "$key" = default ] || line="$line [key=$key]"
line="$line $verb: $note"
if [ $(( ${#line} + 1 )) -gt "$item_bytes" ]; then
suffix=' [truncated]'
keep=$((item_bytes - ${#suffix} - 1))
line="${line:0:$keep}$suffix"
fi
# The shared cut counts the item's own characters; the trailing newline this
# section's global budget also pays for is this caller's, so the per-item
# allowance passed down is one short of the cap.
fm_cap_line_var "$line" $((item_bytes - 1))
line=$FM_LINE_CAP_LINE
bytes=$(( ${#line} + 1 ))
if [ $((used + bytes)) -gt "$global_bytes" ]; then
omitted=$((omitted + 1))
Expand Down
3 changes: 2 additions & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -509,7 +509,8 @@ FM_ZELLIJ_SESSION=firstmate # zellij-only: named session for normal backend ops
FM_BACKEND_CMUX_COMPOSER_LINES=20 # cmux-only: tail lines scanned to locate the composer row for submit verification
FM_BACKEND_CMUX_IDLE_RE='^Type a message\.\.\.$' # cmux-only: empty-composer placeholder regex after border/prompt stripping
CMUX_SOCKET_PASSWORD= # cmux-only: socket password fallback when config/cmux-socket-password is absent (docs/cmux-backend.md)
FM_SESSION_START_STATUS_TAIL=5 # state/*.status lines printed per task in the session-start digest
FM_SESSION_START_STATUS_TAIL=5 # state/*.status lines printed per task in the session-start digest; each line is capped by bin/fm-line-cap-lib.sh
FM_SESSION_START_QUEUED_LIMIT=20 # plain queued backlog rows in the session-start digest; in-flight, held, and blocked rows are never bounded and done rows are never listed
FM_BOOTSTRAP_DETECT_ONLY=0 # internal/read-only session-start mode: skip bootstrap's mutating sweeps and print advisory TANGLE wording
FM_GUARD_READ_ONLY=0 # internal/read-only guard mode: keep alarms but suppress drain, supervision repair, and checkout repair commands
FM_GUARD_CONTINUE_LINE='This is a supervision warning only; the guarded operation WILL still run.' # banner continuation line; fm-send.sh overrides it to name the requested message specifically
Expand Down
Loading
Loading