@rubytech/create-maxy-code 0.1.460 → 0.1.461
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/cron-heartbeat-registration.test.js +57 -28
- package/dist/__tests__/launchd-plist.test.js +26 -0
- package/dist/cron-registration.js +30 -8
- package/dist/index.js +50 -9
- package/dist/launchd-plist.js +1 -1
- package/dist/uninstall.js +9 -3
- package/package.json +1 -1
- package/payload/platform/plugins/cloudflare/bin/portal-enrol.mjs +96 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/auth-route.test.ts +54 -40
- package/payload/platform/plugins/cloudflare/mcp/__tests__/passcode.test.ts +169 -39
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-enrol.test.ts +207 -0
- package/payload/platform/plugins/cloudflare/mcp/package.json +1 -1
- package/payload/platform/plugins/cloudflare/skills/data-portal/SKILL.md +21 -19
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/passcode.mjs +193 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/auth.ts +18 -12
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/schema.sql +14 -5
- package/payload/platform/plugins/voice-mirror/PLUGIN.md +1 -1
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-distil-profile.d.ts.map +1 -1
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-distil-profile.js +98 -9
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-distil-profile.js.map +1 -1
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-retrieve-conditioning.d.ts.map +1 -1
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-retrieve-conditioning.js +54 -21
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-retrieve-conditioning.js.map +1 -1
- package/payload/platform/plugins/voice-mirror/mcp/scripts/smoke.mjs +110 -0
- package/payload/platform/plugins/voice-mirror/skills/voice-mirror/SKILL.md +6 -3
- package/payload/platform/scripts/__tests__/logs-rotate.test.sh +256 -0
- package/payload/platform/scripts/__tests__/resume-tunnel.test.sh +79 -0
- package/payload/platform/scripts/logs-rotate.sh +204 -0
- package/payload/platform/scripts/resume-tunnel.sh +27 -0
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.js +41 -5
- package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/index.d.ts +1 -0
- package/payload/platform/services/claude-session-manager/dist/index.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/index.js +22 -0
- package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/install-start-counter.d.ts +2 -0
- package/payload/platform/services/claude-session-manager/dist/install-start-counter.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/install-start-counter.js +12 -0
- package/payload/platform/services/claude-session-manager/dist/install-start-counter.js.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/pty-census.d.ts +122 -9
- package/payload/platform/services/claude-session-manager/dist/pty-census.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-census.js +214 -29
- package/payload/platform/services/claude-session-manager/dist/pty-census.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +2 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.js +13 -1
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/session-memory-cap.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/session-memory-cap.js +4 -2
- package/payload/platform/services/claude-session-manager/dist/session-memory-cap.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/slice-memory-policy.d.ts +150 -0
- package/payload/platform/services/claude-session-manager/dist/slice-memory-policy.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/slice-memory-policy.js +333 -0
- package/payload/platform/services/claude-session-manager/dist/slice-memory-policy.js.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/start-counter.d.ts +10 -0
- package/payload/platform/services/claude-session-manager/dist/start-counter.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/start-counter.js +55 -0
- package/payload/platform/services/claude-session-manager/dist/start-counter.js.map +1 -0
- package/payload/server/server.js +55 -35
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/passcode.ts +0 -73
|
@@ -619,6 +619,83 @@ case_refuse_on_invalid_state_json() {
|
|
|
619
619
|
return 0
|
|
620
620
|
}
|
|
621
621
|
|
|
622
|
+
# Task 1706 — the rotator's lock must be held across the offset-capture →
|
|
623
|
+
# verification-poll window. logs-rotate.sh truncates cloudflared.log in place
|
|
624
|
+
# when it exceeds 64 MiB; a rotation landing inside this window makes
|
|
625
|
+
# `tail -c "+$LOG_OFFSET"` read past EOF and report a tunnel failure that did
|
|
626
|
+
# not happen.
|
|
627
|
+
case_holds_rotate_lock_during_verify() {
|
|
628
|
+
local root
|
|
629
|
+
root=$(mktemp -d /tmp/resume-tunnel-test-XXXXXX)
|
|
630
|
+
setup "$root" ".maxy-code" "inactive" "yes"
|
|
631
|
+
|
|
632
|
+
local log_dir="$root/home/.maxy-code/logs"
|
|
633
|
+
local marker="$root/lock-was-held"
|
|
634
|
+
|
|
635
|
+
# Poll for the lock's existence while the script runs. The verify gate is the
|
|
636
|
+
# window under test, so a single observation of the lock proves it was held
|
|
637
|
+
# across it.
|
|
638
|
+
( for _ in $(seq 1 60); do
|
|
639
|
+
[[ -d "$log_dir/.rotate.lock" ]] && { touch "$marker"; break; }
|
|
640
|
+
sleep 0.05
|
|
641
|
+
done ) &
|
|
642
|
+
local watcher=$!
|
|
643
|
+
|
|
644
|
+
HOME="$root/home" \
|
|
645
|
+
PATH="$root/bin:$PATH" \
|
|
646
|
+
MAXY_PLATFORM_ROOT="$root/platform_root" \
|
|
647
|
+
bash "$RESUME" >/dev/null 2>&1
|
|
648
|
+
local rc=$?
|
|
649
|
+
wait "$watcher" 2>/dev/null
|
|
650
|
+
|
|
651
|
+
local held=0 leaked=0
|
|
652
|
+
[[ -e "$marker" ]] && held=1
|
|
653
|
+
[[ -d "$log_dir/.rotate.lock" ]] && leaked=1
|
|
654
|
+
cleanup "$root"
|
|
655
|
+
|
|
656
|
+
if [[ $rc -ne 0 ]]; then
|
|
657
|
+
echo " expected exit 0, got $rc"; return 1
|
|
658
|
+
fi
|
|
659
|
+
if [[ $held -ne 1 ]]; then
|
|
660
|
+
echo " rotate lock was never held during the verify window"; return 1
|
|
661
|
+
fi
|
|
662
|
+
if [[ $leaked -ne 0 ]]; then
|
|
663
|
+
echo " rotate lock not released after the verify window"; return 1
|
|
664
|
+
fi
|
|
665
|
+
return 0
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
# A lock already held (by a live rotator tick) must not stop the tunnel coming
|
|
669
|
+
# up. A false-alarm verify line is a far smaller failure than no tunnel.
|
|
670
|
+
case_busy_rotate_lock_does_not_block_spawn() {
|
|
671
|
+
local root
|
|
672
|
+
root=$(mktemp -d /tmp/resume-tunnel-test-XXXXXX)
|
|
673
|
+
setup "$root" ".maxy-code" "inactive" "yes"
|
|
674
|
+
|
|
675
|
+
mkdir -p "$root/home/.maxy-code/logs/.rotate.lock"
|
|
676
|
+
|
|
677
|
+
HOME="$root/home" \
|
|
678
|
+
PATH="$root/bin:$PATH" \
|
|
679
|
+
MAXY_PLATFORM_ROOT="$root/platform_root" \
|
|
680
|
+
bash "$RESUME" >/dev/null 2>&1
|
|
681
|
+
local rc=$?
|
|
682
|
+
|
|
683
|
+
local log_content
|
|
684
|
+
log_content=$(cat "$root/home/.maxy-code/logs/cloudflared.log" 2>/dev/null || echo "")
|
|
685
|
+
cleanup "$root"
|
|
686
|
+
|
|
687
|
+
if [[ $rc -ne 0 ]]; then
|
|
688
|
+
echo " expected exit 0, got $rc"; return 1
|
|
689
|
+
fi
|
|
690
|
+
if ! echo "$log_content" | grep -q "spawned service=cloudflared-maxy-code.service"; then
|
|
691
|
+
echo " a busy rotate lock blocked the tunnel spawn"; echo "$log_content"; return 1
|
|
692
|
+
fi
|
|
693
|
+
if ! echo "$log_content" | grep -q "rotate-lock busy"; then
|
|
694
|
+
echo " proceeding without the lock was not logged"; echo "$log_content"; return 1
|
|
695
|
+
fi
|
|
696
|
+
return 0
|
|
697
|
+
}
|
|
698
|
+
|
|
622
699
|
run_case "skip when service active" case_skip_when_scope_active
|
|
623
700
|
run_case "spawn succeeds and logs verified" case_spawn_succeeds
|
|
624
701
|
run_case "spawn with no connection logs ERROR exits 0" case_spawn_no_connection
|
|
@@ -635,6 +712,8 @@ run_case "reap: skip when active unit's MainPID is unresolved" case_reap_skip_wh
|
|
|
635
712
|
run_case "token tunnel spawns without --origincert" case_token_tunnel_spawns_without_origincert
|
|
636
713
|
run_case "configured tunnel with no run credential refuses loudly" case_refuse_when_no_run_credential
|
|
637
714
|
run_case "corrupt tunnel.state refuses loudly and exits 0" case_refuse_on_invalid_state_json
|
|
715
|
+
run_case "holds the rotate lock during verify" case_holds_rotate_lock_during_verify
|
|
716
|
+
run_case "busy rotate lock does not block spawn" case_busy_rotate_lock_does_not_block_spawn
|
|
638
717
|
|
|
639
718
|
echo ""
|
|
640
719
|
echo "Results: $PASS passed, $FAIL failed"
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Brand log rotation — Task 1706.
|
|
3
|
+
#
|
|
4
|
+
# WHY THIS EXISTS AND NOT logrotate: server.log, edge.log and cloudflared.log
|
|
5
|
+
# are systemd stdout sinks (`StandardOutput=append:`, see
|
|
6
|
+
# packages/create-maxy-code/src/port-resolution.ts:233 and
|
|
7
|
+
# platform/scripts/resume-tunnel.sh:230). systemd opens the file O_APPEND ONCE
|
|
8
|
+
# at unit start and the process holds that fd for its whole life. logrotate's
|
|
9
|
+
# default rename-then-create would leave every service writing into the rotated
|
|
10
|
+
# inode forever — rotation would look like it worked while the live file stayed
|
|
11
|
+
# empty. So we gzip a COPY and truncate IN PLACE: because the fd is O_APPEND,
|
|
12
|
+
# each write seeks to end-of-file first, so writes resume at offset 0 with no
|
|
13
|
+
# service restart and no sparse hole. Verified against a real systemd unit on
|
|
14
|
+
# 2026-07-17 (laptop 192.168.88.10): NRestarts=0, file 71 -> 0 -> 80 bytes for
|
|
15
|
+
# 10 fresh lines. Never replace `: > "$F"` with a mv.
|
|
16
|
+
#
|
|
17
|
+
# Portability: this runs on Linux (cron) and darwin (launchd StartInterval), so
|
|
18
|
+
# no flock(1) (absent on darwin) and no `date +%N` (GNU-only). The lock is a
|
|
19
|
+
# mkdir, which is atomic on both. find's -mmin/-mtime/-delete and the timestamp
|
|
20
|
+
# format were confirmed identical on both platforms.
|
|
21
|
+
#
|
|
22
|
+
# Usage: logs-rotate.sh [LOGS_DIR]
|
|
23
|
+
set -uo pipefail
|
|
24
|
+
|
|
25
|
+
ROTATE_SIZE_BYTES=$((64 * 1024 * 1024))
|
|
26
|
+
KEEP=5
|
|
27
|
+
STALE_LOCK_MINUTES=30
|
|
28
|
+
|
|
29
|
+
# Resolve the logs dir the same way logs-read.sh:52-58 does: convention is
|
|
30
|
+
# configDir = ".${installDir}".
|
|
31
|
+
if [ "$#" -ge 1 ] && [ -n "${1:-}" ]; then
|
|
32
|
+
LOGS_DIR="$1"
|
|
33
|
+
else
|
|
34
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
35
|
+
PLATFORM_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
36
|
+
INSTALL_DIR="$(dirname "$PLATFORM_ROOT")"
|
|
37
|
+
LOGS_DIR="$HOME/.$(basename "$INSTALL_DIR")/logs"
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
# Absent logs dir is not an error: the installer creates it, and a rotator tick
|
|
41
|
+
# that lands before the first install completes has nothing to do.
|
|
42
|
+
[ -d "$LOGS_DIR" ] || exit 0
|
|
43
|
+
|
|
44
|
+
LOCK="$LOGS_DIR/.rotate.lock"
|
|
45
|
+
|
|
46
|
+
stamp() { date -u +%Y-%m-%dT%H:%M:%SZ; }
|
|
47
|
+
|
|
48
|
+
# A mkdir lock is NOT released by the kernel when its holder dies, so a crashed
|
|
49
|
+
# rotator would wedge rotation permanently and silently — worse than the race
|
|
50
|
+
# the lock prevents. Reclaim on age: 30 min is two rotator ticks and 90x
|
|
51
|
+
# resume-tunnel.sh's 20s window, so no live holder is mistaken for a dead one.
|
|
52
|
+
#
|
|
53
|
+
# The reclaim moves the stale lock aside with `mv` before recreating it, rather
|
|
54
|
+
# than `rm -rf`-then-mkdir. `mv` is the atomic gate: if two ticks both see the
|
|
55
|
+
# same stale lock, only one can move that inode — the loser's `mv` fails because
|
|
56
|
+
# the source is already gone, and it backs off. rm-then-mkdir had no such gate:
|
|
57
|
+
# the loser would delete the winner's brand-new lock and both would rotate,
|
|
58
|
+
# shifting generations twice for one new .1.gz.
|
|
59
|
+
acquire_lock() {
|
|
60
|
+
if mkdir "$LOCK" 2>/dev/null; then return 0; fi
|
|
61
|
+
if [ -d "$LOCK" ] && [ -z "$(find "$LOCK" -maxdepth 0 -mmin -"$STALE_LOCK_MINUTES" 2>/dev/null)" ]; then
|
|
62
|
+
local stale="$LOCK.stale.$$"
|
|
63
|
+
mv "$LOCK" "$stale" 2>/dev/null || return 1 # lost the reclaim race
|
|
64
|
+
rm -rf "$stale" 2>/dev/null
|
|
65
|
+
echo "$(stamp) [logs-rotate] stale-lock-reclaimed lock=$LOCK age-min=>$STALE_LOCK_MINUTES"
|
|
66
|
+
mkdir "$LOCK" 2>/dev/null && return 0 # a third tick may have won; then back off
|
|
67
|
+
fi
|
|
68
|
+
return 1
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if ! acquire_lock; then
|
|
72
|
+
# Another rotator tick, or a resume-tunnel.sh verification window, is live.
|
|
73
|
+
# Exit 0: the next tick is 15 min away and the bound is not tight enough for
|
|
74
|
+
# a skipped tick to matter.
|
|
75
|
+
echo "$(stamp) [logs-rotate] skipped reason=lock-held lock=$LOCK"
|
|
76
|
+
exit 0
|
|
77
|
+
fi
|
|
78
|
+
trap 'rmdir "$LOCK" 2>/dev/null || true' EXIT
|
|
79
|
+
|
|
80
|
+
size_of() { wc -c < "$1" 2>/dev/null | awk '{print $1+0}'; }
|
|
81
|
+
|
|
82
|
+
# Size-rotate one file: gzip a copy to a temp, shift older generations, move the
|
|
83
|
+
# temp into .1.gz, truncate in place, then mark the truncation so the file is
|
|
84
|
+
# never mistaken for one that simply starts there.
|
|
85
|
+
#
|
|
86
|
+
# ORDER IS LOAD-BEARING: gzip FIRST, shift only once it succeeded. The shift used
|
|
87
|
+
# to run first, so a gzip failure moved .4->.5 ... .1->.2 and wrote nothing to
|
|
88
|
+
# .1 — dropping the oldest generation and leaving a hole, on every tick. The
|
|
89
|
+
# trigger is ENOSPC, i.e. precisely the condition rotation exists to prevent: 75
|
|
90
|
+
# minutes of a full disk destroyed 4 of the 5 retained generations while
|
|
91
|
+
# correctly refusing to truncate. Never hoist the shift above the gzip.
|
|
92
|
+
rotate_file() {
|
|
93
|
+
local f="$1" base size i prev
|
|
94
|
+
[ -f "$f" ] || return 0
|
|
95
|
+
size="$(size_of "$f")"
|
|
96
|
+
[ "$size" -gt "$ROTATE_SIZE_BYTES" ] || return 0
|
|
97
|
+
base="$(basename "$f")"
|
|
98
|
+
|
|
99
|
+
# gzip to a temp first, so a failure (or a tick killed mid-compress) costs
|
|
100
|
+
# nothing: no generation has moved and the live file is untouched.
|
|
101
|
+
#
|
|
102
|
+
# gzip's stderr is CAPTURED, not discarded, and relayed verbatim on failure —
|
|
103
|
+
# it is the only thing that distinguishes ENOSPC from EACCES from a missing
|
|
104
|
+
# binary. `2>&1 >file` sends stderr to the command substitution and stdout to
|
|
105
|
+
# the temp; the order matters.
|
|
106
|
+
#
|
|
107
|
+
# A live log grows while gzip reads it, so gzip emits "file size changed while
|
|
108
|
+
# zipping" on nearly every real rotation. That is a WARNING and gzip still
|
|
109
|
+
# exits 0 (verified on the laptop against a file being appended to during the
|
|
110
|
+
# read), so it takes the success path and the noise is never logged. Only a
|
|
111
|
+
# genuine non-zero exit reaches the branch below.
|
|
112
|
+
local gzerr
|
|
113
|
+
gzerr="$(gzip -c "$f" 2>&1 >"$f.1.gz.tmp")"
|
|
114
|
+
if [ $? -ne 0 ]; then
|
|
115
|
+
rm -f "$f.1.gz.tmp"
|
|
116
|
+
echo "$(stamp) [logs-rotate] ERROR file=$base reason=gzip-failed bytes=$size err=$(printf '%s' "$gzerr" | tr '\n' ' ') — not truncated, generations intact"
|
|
117
|
+
return 1
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
# Shift down from the oldest so nothing is overwritten before it moves. The
|
|
121
|
+
# KEEP-th generation is overwritten by the (KEEP-1)-th, which is how the
|
|
122
|
+
# oldest falls off.
|
|
123
|
+
i="$KEEP"
|
|
124
|
+
while [ "$i" -gt 1 ]; do
|
|
125
|
+
prev=$((i - 1))
|
|
126
|
+
[ -f "$f.$prev.gz" ] && mv -f "$f.$prev.gz" "$f.$i.gz"
|
|
127
|
+
i="$prev"
|
|
128
|
+
done
|
|
129
|
+
mv -f "$f.1.gz.tmp" "$f.1.gz"
|
|
130
|
+
|
|
131
|
+
# THE load-bearing line. Truncate in place; do not mv. See the header.
|
|
132
|
+
#
|
|
133
|
+
# Lines written between gzip reaching EOF and this truncate are LOST. gzip
|
|
134
|
+
# reads to the CURRENT end of file, so the window is the shift plus two mvs —
|
|
135
|
+
# milliseconds, not the seconds the gzip itself takes. Inherent to
|
|
136
|
+
# copy-truncate, which the O_APPEND fd leaves no way around.
|
|
137
|
+
#
|
|
138
|
+
# `size` was sampled BEFORE gzip ran, so on a live log the archive legitimately
|
|
139
|
+
# holds slightly MORE than prev-bytes reports. prev-bytes locates the seam
|
|
140
|
+
# approximately, not exactly. See .docs/brand-log-retention.md.
|
|
141
|
+
: > "$f"
|
|
142
|
+
echo "$(stamp) [logs-rotate] rotated file=$base prev-bytes=$size prev-file=$base.1.gz keep=$KEEP" >> "$f"
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
PURGE_DAYS=7
|
|
146
|
+
|
|
147
|
+
for f in server.log edge.log cloudflared.log check-due-events.log; do
|
|
148
|
+
rotate_file "$LOGS_DIR/$f"
|
|
149
|
+
done
|
|
150
|
+
|
|
151
|
+
# Globbed size targets. nullglob so a brand with none of these yet does not try
|
|
152
|
+
# to rotate a literal '*' path.
|
|
153
|
+
#
|
|
154
|
+
# mcp-*-stderr-*.log: the tee holds a live write stream, so this is the ONLY
|
|
155
|
+
# bound it gets here — see the purge note below for why it is never age-purged.
|
|
156
|
+
#
|
|
157
|
+
# rc-child-*.log: its 16 MiB writer cap does NOT hold. `written`
|
|
158
|
+
# (rc-child-log.ts:33) is in-memory per manager process, so a manager restart
|
|
159
|
+
# resumes appending to the same path with a fresh counter. Observed at 54 MB on
|
|
160
|
+
# sitedesk-code, 3.25x the cap. A long-lived session's file also has a fresh
|
|
161
|
+
# mtime forever, so the age purge below never reaches it — without size rotation
|
|
162
|
+
# it is genuinely unbounded.
|
|
163
|
+
shopt -s nullglob
|
|
164
|
+
for f in "$LOGS_DIR"/mcp-*-stderr-*.log "$LOGS_DIR"/rc-child-*.log; do
|
|
165
|
+
rotate_file "$f"
|
|
166
|
+
done
|
|
167
|
+
shopt -u nullglob
|
|
168
|
+
|
|
169
|
+
# Age purge — rc-child only.
|
|
170
|
+
#
|
|
171
|
+
# rc-child-*.log is one file per session and the set grows without bound (100 on
|
|
172
|
+
# sitedesk-code). Purging it is SAFE because rc-child-log.ts:51 writes with
|
|
173
|
+
# appendFileSync: no fd is held, so a purged file is simply recreated on the
|
|
174
|
+
# next write.
|
|
175
|
+
#
|
|
176
|
+
# mcp-*-stderr-*.log is deliberately NOT purged, though an earlier draft did.
|
|
177
|
+
# The tee (mcp-stderr-tee/src/index.ts:88) opens a createWriteStream at process
|
|
178
|
+
# start and holds it for the process's whole life, writing only when the server
|
|
179
|
+
# emits stderr — which a healthy server does not do for days. So a LIVE file's
|
|
180
|
+
# mtime DOES go stale, and deleting it would send every later stderr write to an
|
|
181
|
+
# unlinked inode: invisible exactly when the fault you are chasing finally
|
|
182
|
+
# appears, and its disk unreclaimed until the process exits. Bounding these by
|
|
183
|
+
# age needs live-vs-dead discrimination, which needs the writer to stop pinning
|
|
184
|
+
# its date at boot — that is Task 1721. Size rotation above is safe regardless,
|
|
185
|
+
# because truncate-in-place is precisely what a held fd tolerates.
|
|
186
|
+
#
|
|
187
|
+
# The pattern is `.log*` so a purged session's rotated .gz generations go too.
|
|
188
|
+
# `find` matches each path by its own mtime — there is no sibling relationship —
|
|
189
|
+
# so a LIVE rc-child's generations also age out at 7 days. That is intended:
|
|
190
|
+
# retention for rc-child is min(KEEP, 7 days), and 7 days of a session's spawn
|
|
191
|
+
# output is the stated bound.
|
|
192
|
+
# One traversal, and the count is what was actually deleted rather than what a
|
|
193
|
+
# separate counting pass saw a moment earlier. `-print` before `-delete` prints
|
|
194
|
+
# each path as it goes.
|
|
195
|
+
purge_old() {
|
|
196
|
+
local pattern="$1" n
|
|
197
|
+
n="$(find "$LOGS_DIR" -maxdepth 1 -name "$pattern" -type f -mtime +"$PURGE_DAYS" -print -delete 2>/dev/null | wc -l | awk '{print $1+0}')"
|
|
198
|
+
[ "$n" -gt 0 ] || return 0
|
|
199
|
+
echo "$(stamp) [logs-rotate] purged pattern=$pattern files=$n older-than-days=$PURGE_DAYS"
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
purge_old 'rc-child-*.log*'
|
|
203
|
+
|
|
204
|
+
exit 0
|
|
@@ -194,6 +194,24 @@ log "Resuming tunnel $TUNNEL_ID for $DOMAIN..."
|
|
|
194
194
|
# Capture log byte offset before spawning so the verification poll only
|
|
195
195
|
# matches lines written by THIS invocation, not lines from a prior run.
|
|
196
196
|
mkdir -p "$LOG_DIR"
|
|
197
|
+
|
|
198
|
+
# Task 1706 — hold the log rotator's lock across offset-capture → verify-poll.
|
|
199
|
+
# logs-rotate.sh truncates cloudflared.log in place when it exceeds 64 MiB; a
|
|
200
|
+
# rotation landing inside this window makes `tail -c "+$LOG_OFFSET"` read past
|
|
201
|
+
# EOF and report a tunnel failure that did not happen. Same mkdir primitive as
|
|
202
|
+
# the rotator (no flock(1) on darwin, and a lock is only a lock if both ends
|
|
203
|
+
# agree). Best-effort: if the lock cannot be taken we proceed anyway rather than
|
|
204
|
+
# refuse to bring the tunnel up — a false-alarm log line is a far smaller
|
|
205
|
+
# failure than a tunnel that never starts.
|
|
206
|
+
ROTATE_LOCK="$LOG_DIR/.rotate.lock"
|
|
207
|
+
ROTATE_LOCK_HELD=0
|
|
208
|
+
if mkdir "$ROTATE_LOCK" 2>/dev/null; then
|
|
209
|
+
ROTATE_LOCK_HELD=1
|
|
210
|
+
trap 'rmdir "$ROTATE_LOCK" 2>/dev/null || true' EXIT
|
|
211
|
+
else
|
|
212
|
+
log "rotate-lock busy — proceeding without it; a concurrent rotation may skew the verify gate"
|
|
213
|
+
fi
|
|
214
|
+
|
|
197
215
|
LOG_OFFSET=$(wc -c < "$LOG_FILE" 2>/dev/null | awk '{print $1+0}' || echo 0)
|
|
198
216
|
|
|
199
217
|
# Spawn into a SUPERVISED transient service under cloudflared.slice (Task 757).
|
|
@@ -264,6 +282,15 @@ else
|
|
|
264
282
|
log "ERROR: tunnel service started but no edge connection within ${VERIFY_DEADLINE}s — service=$SERVICE_UNIT"
|
|
265
283
|
fi
|
|
266
284
|
|
|
285
|
+
# Task 1706 — release the rotator's lock. The verify gate above is the only
|
|
286
|
+
# offset-sensitive window, so holding it past here would make a rotator tick
|
|
287
|
+
# skip for no reason.
|
|
288
|
+
if [ "$ROTATE_LOCK_HELD" -eq 1 ]; then
|
|
289
|
+
rmdir "$ROTATE_LOCK" 2>/dev/null || true
|
|
290
|
+
ROTATE_LOCK_HELD=0
|
|
291
|
+
trap - EXIT
|
|
292
|
+
fi
|
|
293
|
+
|
|
267
294
|
# Update state file: keep pid/startedAt for observability, add serviceUnit as control signal.
|
|
268
295
|
NEW_STATE=$(echo "$STATE" | jq \
|
|
269
296
|
--arg unit "$SERVICE_UNIT" \
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http-server.d.ts","sourceRoot":"","sources":["../src/http-server.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"http-server.d.ts","sourceRoot":"","sources":["../src/http-server.ts"],"names":[],"mappings":"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"}
|
|
@@ -3306,7 +3306,9 @@ export function buildHttpApp(deps) {
|
|
|
3306
3306
|
// non-allowlisted channel like whatsapp) prints an interactive prompt
|
|
3307
3307
|
// ("Loading development channels … ❯ 1. I am using this for local
|
|
3308
3308
|
// development … Enter to confirm") that a programmatic spawn cannot answer,
|
|
3309
|
-
// hanging the boot until
|
|
3309
|
+
// hanging the boot until the bind wait elapses (a blocked child writes no
|
|
3310
|
+
// pid file, so Task 1716 classifies it `outcome=failed`, and `blocker=` on
|
|
3311
|
+
// op=bind-signal-absent names the screen). Detect it once and send Enter to
|
|
3310
3312
|
// select option 1. The prompt is laid out with cursor-position escapes and
|
|
3311
3313
|
// no literal spaces, so match on the ANSI/space-stripped letter stream.
|
|
3312
3314
|
// Bounded startup keystroke only — inbound message submission stays a
|
|
@@ -3359,7 +3361,12 @@ export function buildHttpApp(deps) {
|
|
|
3359
3361
|
// by `registerRcSpawnPty`) handles the tracker side — fd release +
|
|
3360
3362
|
// tracker delete + the `kill … reason=process-exited` log.
|
|
3361
3363
|
let pidFileSeen = false;
|
|
3364
|
+
// Task 1716 — the second half of the bind verdict's discriminator. Set
|
|
3365
|
+
// first, so a child that has already gone can never be read as still
|
|
3366
|
+
// running by the decision below.
|
|
3367
|
+
let ptyExited = false;
|
|
3362
3368
|
pty.onExit((e) => {
|
|
3369
|
+
ptyExited = true;
|
|
3363
3370
|
const ranMs = Date.now() - start;
|
|
3364
3371
|
if (!pidFileSeen) {
|
|
3365
3372
|
deps.logger(`[rc-spawn] op=early-exit unitToken=${unitToken} pid=${pty.pid} ranMs=${ranMs} exitCode=${e.exitCode} signal=${e.signal ?? 'none'}`);
|
|
@@ -3490,8 +3497,33 @@ export function buildHttpApp(deps) {
|
|
|
3490
3497
|
// through the proxy unchanged so the client decides modal-vs-navigate
|
|
3491
3498
|
// from one server-supplied value.
|
|
3492
3499
|
const waitedMs = Date.now() - start;
|
|
3493
|
-
|
|
3494
|
-
|
|
3500
|
+
// Task 1716 — the bind signal certifies that a remote-control BRIDGE
|
|
3501
|
+
// exists, not that the session is usable. Device evidence 2026-07-17: a
|
|
3502
|
+
// child can attach its channel, drain and serve a full turn while
|
|
3503
|
+
// registering no bridge at all — no stdout banner, and a pid file carrying
|
|
3504
|
+
// every field except `bridgeSessionId`. Both signals then correctly report
|
|
3505
|
+
// "no bridge", so slug-absence alone cannot mean the spawn failed.
|
|
3506
|
+
//
|
|
3507
|
+
// The verdict is therefore derived from the child's real state at decision
|
|
3508
|
+
// time, which is why a "failed" verdict cannot coexist with a healthy
|
|
3509
|
+
// session by construction:
|
|
3510
|
+
// bound — a slug arrived on either signal.
|
|
3511
|
+
// unbound — no slug, but the child wrote its pid file and is still
|
|
3512
|
+
// running: usable, simply not navigable from claude.ai/code.
|
|
3513
|
+
// failed — no slug and the child never became a running registered
|
|
3514
|
+
// session (it died, or is stuck on a blocking screen; the
|
|
3515
|
+
// `blocker=` field on op=bind-signal-absent names which).
|
|
3516
|
+
const outcome = bridgeSessionId
|
|
3517
|
+
? 'bound'
|
|
3518
|
+
: pidFileSeen && !ptyExited
|
|
3519
|
+
? 'unbound'
|
|
3520
|
+
: 'failed';
|
|
3521
|
+
// `reason` is the string the client renders in its error modal
|
|
3522
|
+
// (Sidebar.tsx::resolveRcSpawnOutcome). Neither value names a pid bind:
|
|
3523
|
+
// the wait has been on the stdout banner since Task 662, and the old
|
|
3524
|
+
// `pid-bind-timeout` sent every reader to the wrong mechanism. Nor does
|
|
3525
|
+
// `spawn-not-ready` claim an exit — a blocked-but-alive child never exits.
|
|
3526
|
+
const reason = outcome === 'bound' ? null : outcome === 'unbound' ? 'bridge-unregistered' : 'spawn-not-ready';
|
|
3495
3527
|
// Task 1406 — derive the child's first-turn auth verdict. A bound child
|
|
3496
3528
|
// authenticated; an unbound child that printed `Not logged in` is the
|
|
3497
3529
|
// file-healthy-but-child-unauthenticated signature this task closes. Emitted
|
|
@@ -3507,8 +3539,12 @@ export function buildHttpApp(deps) {
|
|
|
3507
3539
|
// file produced a slug within the bound. This distinct marker names the
|
|
3508
3540
|
// artifact the manager waited for and the CLI version that produced
|
|
3509
3541
|
// neither, so a future CLI drift is read from one log line instead of
|
|
3510
|
-
// re-derived by reproduction.
|
|
3511
|
-
//
|
|
3542
|
+
// re-derived by reproduction.
|
|
3543
|
+
//
|
|
3544
|
+
// Task 1716 — this line fires for BOTH non-bound outcomes, which are not
|
|
3545
|
+
// the same event: `stdoutBytes` + `pidFileSeen` are what separate a dead
|
|
3546
|
+
// child (near-silent, no pid file) from a healthy unbound one (chatty,
|
|
3547
|
+
// pid file present, serving turns). Read them before concluding drift.
|
|
3512
3548
|
//
|
|
3513
3549
|
// Task 664 — scan the captured child stdout for the known blocking-screen
|
|
3514
3550
|
// markers and name the matched one (`blocker=`). `bindBuf` holds the 8 KB
|