aiur-cli 0.0.8 → 0.0.9-nightly.1de41ef
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/bin/aiur.js +37 -17
- package/libexec/aiur-capabilities.sh +12 -0
- package/libexec/aiur-daemon-supervisor.sh +96 -0
- package/libexec/aiur-engine.sh +134 -3990
- package/libexec/aiur-epic.sh +41 -0
- package/libexec/aiur-queue.sh +107 -0
- package/libexec/engine/commands.sh +297 -0
- package/libexec/engine/control.sh +459 -0
- package/libexec/engine/dotenv.sh +88 -0
- package/libexec/engine/executor_commands.sh +291 -0
- package/libexec/engine/identity.sh +377 -0
- package/libexec/engine/instance.sh +226 -0
- package/libexec/engine/lifecycle.sh +458 -0
- package/libexec/engine/oneshot.sh +173 -0
- package/libexec/engine/processes.sh +270 -0
- package/libexec/engine/reap.sh +262 -0
- package/libexec/engine/report_commands.sh +293 -0
- package/libexec/engine/run.sh +471 -0
- package/libexec/engine/session.sh +204 -0
- package/libexec/engine/upgrade.sh +220 -0
- package/package.json +9 -4
package/libexec/aiur-engine.sh
CHANGED
|
@@ -20,4001 +20,121 @@
|
|
|
20
20
|
#
|
|
21
21
|
# The release is self-contained (bundled ERTS), so it runs without mise/Elixir on
|
|
22
22
|
# PATH. Dev's build-if-stale step lives in the aiurdev shim, not here.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
ulimit -Sn
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
# hands daemon ownership to its pane launcher.
|
|
53
|
-
if ! [[ "${AIUR_OPERATOR_PID:-}" =~ ^[1-9][0-9]*$ ]]; then
|
|
54
|
-
if [[ "${PPID:-}" =~ ^[1-9][0-9]*$ ]]; then
|
|
55
|
-
export AIUR_OPERATOR_PID="$PPID"
|
|
56
|
-
else
|
|
57
|
-
unset AIUR_OPERATOR_PID
|
|
58
|
-
fi
|
|
59
|
-
fi
|
|
60
|
-
|
|
61
|
-
die() {
|
|
62
|
-
echo "❌ $*" >&2
|
|
63
|
-
exit 1
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
legacy_config_path() {
|
|
67
|
-
while [ "$#" -gt 0 ]; do
|
|
68
|
-
case "$1" in
|
|
69
|
-
--logs-root | --host | --port | --max-agents)
|
|
70
|
-
if [ "$#" -ge 2 ]; then
|
|
71
|
-
shift 2
|
|
72
|
-
else
|
|
73
|
-
shift
|
|
74
|
-
fi
|
|
75
|
-
;;
|
|
76
|
-
--)
|
|
77
|
-
shift
|
|
78
|
-
[ "$#" -gt 0 ] || return
|
|
79
|
-
case "$1" in
|
|
80
|
-
*.aiurconfig) printf '%s' "$1" ;;
|
|
81
|
-
esac
|
|
82
|
-
return
|
|
83
|
-
;;
|
|
84
|
-
--logs-root=* | --host=* | --port=* | --max-agents=* | --*)
|
|
85
|
-
shift
|
|
86
|
-
;;
|
|
87
|
-
*.aiurconfig)
|
|
88
|
-
printf '%s' "$1"
|
|
89
|
-
return
|
|
90
|
-
;;
|
|
91
|
-
*)
|
|
92
|
-
# A positional non-legacy config is authoritative. Let the Elixir CLI
|
|
93
|
-
# validate it instead of rejecting an unrelated ambient legacy file.
|
|
94
|
-
return
|
|
95
|
-
;;
|
|
96
|
-
esac
|
|
97
|
-
done
|
|
98
|
-
|
|
99
|
-
local target_root="${AIUR_REPO_ROOT:-}" home_real
|
|
100
|
-
home_real="$(cd "${HOME:-}" 2>/dev/null && pwd -P || printf '%s' "${HOME:-}")"
|
|
101
|
-
|
|
102
|
-
if [ -n "$target_root" ]; then
|
|
103
|
-
if [ -f "$target_root/.aiur/config" ]; then
|
|
104
|
-
return
|
|
105
|
-
fi
|
|
106
|
-
if [ -f "$target_root/.aiurconfig" ]; then
|
|
107
|
-
printf '%s' "$target_root/.aiurconfig"
|
|
108
|
-
return
|
|
109
|
-
fi
|
|
110
|
-
else
|
|
111
|
-
local pwd_real d
|
|
112
|
-
pwd_real="$(pwd -P 2>/dev/null || printf '%s' "$PWD")"
|
|
113
|
-
d="$pwd_real"
|
|
114
|
-
|
|
115
|
-
while [ -n "$d" ] && [ "$d" != "/" ] && [ "$d" != "$home_real" ]; do
|
|
116
|
-
if [ -f "$d/.aiur/config" ]; then
|
|
117
|
-
return
|
|
118
|
-
fi
|
|
119
|
-
if [ -f "$d/.aiurconfig" ]; then
|
|
120
|
-
printf '%s' "$d/.aiurconfig"
|
|
121
|
-
return
|
|
122
|
-
fi
|
|
123
|
-
d="$(dirname "$d")"
|
|
124
|
-
done
|
|
125
|
-
fi
|
|
126
|
-
|
|
127
|
-
if [ -n "$home_real" ] && [ ! -f "$home_real/.aiur/config" ] && [ -f "$home_real/.aiurconfig" ]; then
|
|
128
|
-
printf '%s' "$home_real/.aiurconfig"
|
|
129
|
-
fi
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
reject_legacy_config() {
|
|
133
|
-
local legacy legacy_basename canonical
|
|
134
|
-
legacy="$(legacy_config_path "$@")"
|
|
135
|
-
[ -z "$legacy" ] && return
|
|
136
|
-
legacy_basename="${legacy##*/}"
|
|
137
|
-
|
|
138
|
-
if [ "$legacy_basename" = ".aiurconfig" ]; then
|
|
139
|
-
canonical="$(dirname "$legacy")/.aiur/config"
|
|
140
|
-
else
|
|
141
|
-
canonical="${legacy%.aiurconfig}.yaml"
|
|
142
|
-
fi
|
|
143
|
-
|
|
144
|
-
die "$legacy is no longer supported. Move it to $canonical. Keep relative prompt_file and hooks_file paths valid from the new config directory."
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
engine_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
148
|
-
|
|
149
|
-
package_version() {
|
|
150
|
-
local package_file="$1"
|
|
151
|
-
[ -r "$package_file" ] || return 1
|
|
152
|
-
sed -n 's/^[[:space:]]*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$package_file" | head -n 1
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
cli_package_version() {
|
|
156
|
-
package_version "$engine_dir/../package.json"
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
version_is_older() {
|
|
160
|
-
local installed="$1" available="$2"
|
|
161
|
-
local installed_major installed_minor installed_patch installed_pre
|
|
162
|
-
local available_major available_minor available_patch available_pre
|
|
163
|
-
|
|
164
|
-
if [[ ! "$installed" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)(-([0-9A-Za-z.-]+))?(\+[0-9A-Za-z.-]+)?$ ]]; then return 1; fi
|
|
165
|
-
installed_major="${BASH_REMATCH[1]}"
|
|
166
|
-
installed_minor="${BASH_REMATCH[2]}"
|
|
167
|
-
installed_patch="${BASH_REMATCH[3]}"
|
|
168
|
-
installed_pre="${BASH_REMATCH[5]}"
|
|
169
|
-
|
|
170
|
-
if [[ ! "$available" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)(-([0-9A-Za-z.-]+))?(\+[0-9A-Za-z.-]+)?$ ]]; then return 1; fi
|
|
171
|
-
available_major="${BASH_REMATCH[1]}"
|
|
172
|
-
available_minor="${BASH_REMATCH[2]}"
|
|
173
|
-
available_patch="${BASH_REMATCH[3]}"
|
|
174
|
-
available_pre="${BASH_REMATCH[5]}"
|
|
175
|
-
|
|
176
|
-
[ "$installed_major" -lt "$available_major" ] ||
|
|
177
|
-
{ [ "$installed_major" -eq "$available_major" ] && [ "$installed_minor" -lt "$available_minor" ]; } ||
|
|
178
|
-
{ [ "$installed_major" -eq "$available_major" ] && [ "$installed_minor" -eq "$available_minor" ] && [ "$installed_patch" -lt "$available_patch" ]; } ||
|
|
179
|
-
{
|
|
180
|
-
[ "$installed_major" -eq "$available_major" ] &&
|
|
181
|
-
[ "$installed_minor" -eq "$available_minor" ] &&
|
|
182
|
-
[ "$installed_patch" -eq "$available_patch" ] &&
|
|
183
|
-
{
|
|
184
|
-
{ [ -n "$installed_pre" ] && [ -z "$available_pre" ]; } ||
|
|
185
|
-
{ [ -n "$installed_pre" ] && [ -n "$available_pre" ] && [[ "$installed_pre" < "$available_pre" ]]; }
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
warn_if_cli_behind_release_checkout() {
|
|
191
|
-
local stamp="${AIUR_RELEASE_DIR:-}/AIUR_BUILD_STAMP"
|
|
192
|
-
local repo_root installed_version checkout_version
|
|
193
|
-
|
|
194
|
-
[ -r "$stamp" ] || return 0
|
|
195
|
-
repo_root="$(sed -n 's/^repo_root=//p' "$stamp" | head -n 1)"
|
|
196
|
-
[ -n "$repo_root" ] || return 0
|
|
197
|
-
|
|
198
|
-
installed_version="$(cli_package_version || true)"
|
|
199
|
-
checkout_version="$(package_version "$repo_root/packaging/npm/aiur-cli/package.json" || true)"
|
|
200
|
-
|
|
201
|
-
if version_is_older "$installed_version" "$checkout_version"; then
|
|
202
|
-
echo "aiur: installed CLI $installed_version is older than checkout CLI $checkout_version; update aiur-cli before retrying" >&2
|
|
203
|
-
fi
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
# --- distribution identity (per-instance: keyed by the aiur project root) -----
|
|
207
|
-
|
|
208
|
-
# The aiur project root used to key this instance. AIUR_REPO_ROOT (set by the dev
|
|
209
|
-
# shim) wins. Otherwise walk up from $PWD to the first dir holding a REPO-LOCAL
|
|
210
|
-
# config — but the walk STOPS at $HOME: the global config at ~/.aiur/config is
|
|
211
|
-
# not a repo root, and treating it as one would collapse every
|
|
212
|
-
# project under $HOME onto one key (#443). When no repo-local config is found, the
|
|
213
|
-
# BEAM serves this run via the global config (mirroring its discovery order in
|
|
214
|
-
# src/lib/aiur/workflow.ex) — or via none — and in both cases the project being
|
|
215
|
-
# served is the cwd, so we key by realpath($PWD). That gives each global-config
|
|
216
|
-
# project a distinct identity instead of an empty key (legacy aiur-$USER@…) or $HOME.
|
|
217
|
-
#
|
|
218
|
-
# Caveat: a global-config run's key is cwd-derived (there is no repo root to
|
|
219
|
-
# converge on), so control commands (status/pause/stop) must be run from the SAME
|
|
220
|
-
# directory the run was launched from. A repo with a repo-local .aiur/config keeps
|
|
221
|
-
# the walk-up, so its control commands still resolve from any subdir.
|
|
222
|
-
aiur_project_root() {
|
|
223
|
-
if [ -n "${AIUR_REPO_ROOT:-}" ]; then printf '%s' "$AIUR_REPO_ROOT"; return; fi
|
|
224
|
-
|
|
225
|
-
# Canonicalize $PWD and $HOME so the home-boundary test below holds even when
|
|
226
|
-
# either is reached through a symlink.
|
|
227
|
-
local pwd_real home_real
|
|
228
|
-
pwd_real="$(pwd -P 2>/dev/null || printf '%s' "$PWD")"
|
|
229
|
-
# ${HOME:-} (not bare $HOME) so an unset HOME under `set -u` can't abort the
|
|
230
|
-
# script here; an empty home_real simply disables the boundary (walk to /).
|
|
231
|
-
home_real="$(cd "${HOME:-}" 2>/dev/null && pwd -P || printf '%s' "${HOME:-}")"
|
|
232
|
-
|
|
233
|
-
local d="$pwd_real"
|
|
234
|
-
while [ -n "$d" ] && [ "$d" != "/" ] && [ "$d" != "$home_real" ]; do
|
|
235
|
-
if [ -f "$d/.aiur/config" ]; then
|
|
236
|
-
printf '%s' "$d"
|
|
237
|
-
return
|
|
238
|
-
fi
|
|
239
|
-
d="$(dirname "$d")"
|
|
240
|
-
done
|
|
241
|
-
|
|
242
|
-
printf '%s' "$pwd_real"
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
aiur_project_root_source() {
|
|
246
|
-
if [ -n "${AIUR_REPO_ROOT:-}" ]; then printf 'env'; return; fi
|
|
247
|
-
|
|
248
|
-
local pwd_real home_real
|
|
249
|
-
pwd_real="$(pwd -P 2>/dev/null || printf '%s' "$PWD")"
|
|
250
|
-
home_real="$(cd "${HOME:-}" 2>/dev/null && pwd -P || printf '%s' "${HOME:-}")"
|
|
251
|
-
|
|
252
|
-
local d="$pwd_real"
|
|
253
|
-
while [ -n "$d" ] && [ "$d" != "/" ] && [ "$d" != "$home_real" ]; do
|
|
254
|
-
if [ -f "$d/.aiur/config" ]; then
|
|
255
|
-
printf 'repo'
|
|
256
|
-
return
|
|
257
|
-
fi
|
|
258
|
-
d="$(dirname "$d")"
|
|
259
|
-
done
|
|
260
|
-
|
|
261
|
-
printf 'cwd'
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
# Short, stable, node-name-legal (lowercase hex) key for the project root, so two
|
|
265
|
-
# aiur instances for the same user get distinct node/session/socket names and can't
|
|
266
|
-
# reap each other. Any real cwd now resolves a key (global-config runs key by
|
|
267
|
-
# realpath($PWD), #443); only a degenerate unreadable cwd yields empty, falling back
|
|
268
|
-
# to the legacy un-keyed name.
|
|
269
|
-
aiur_instance_key() {
|
|
270
|
-
local root
|
|
271
|
-
root="$(aiur_project_root)"
|
|
272
|
-
[ -n "$root" ] || return 0
|
|
273
|
-
# Canonicalize (resolve symlinks) so launch and control commands invoked via
|
|
274
|
-
# different logical paths to the same project agree on the key. Fall back to the
|
|
275
|
-
# literal path when the dir doesn't exist (e.g. a test-fixture root).
|
|
276
|
-
root="$(cd "$root" 2>/dev/null && pwd -P || printf '%s' "$root")"
|
|
277
|
-
printf '%s' "$root" | { shasum -a 256 2>/dev/null || sha256sum; } | cut -c1-10
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
aiur_resolve_identity() {
|
|
281
|
-
local config_home="${XDG_CONFIG_HOME:-$HOME/.config}"
|
|
282
|
-
AIUR_PROJECT_ROOT="$(aiur_project_root)"
|
|
283
|
-
AIUR_PROJECT_ROOT_SOURCE="$(aiur_project_root_source)"
|
|
284
|
-
|
|
285
|
-
: "${AIUR_BG_STATE_DIR:=$config_home/aiur}"
|
|
286
|
-
: "${AIUR_COOKIE_FILE:=$AIUR_BG_STATE_DIR/cookie}"
|
|
287
|
-
: "${AIUR_SESSION_PREFIX:=aiur}"
|
|
288
|
-
: "${AIUR_PROFILES_FILE:=$config_home/aiur/aiur.profiles}"
|
|
289
|
-
# Compute the per-instance key once. `${VAR+x}` so an explicit empty value (no
|
|
290
|
-
# project resolved) is honored and not recomputed on each call.
|
|
291
|
-
if [ -z "${AIUR_INSTANCE_KEY+x}" ]; then
|
|
292
|
-
AIUR_INSTANCE_KEY="$(aiur_instance_key)"
|
|
293
|
-
fi
|
|
294
|
-
: "${AIUR_RELEASE_NODE:=aiur-${USER}${AIUR_INSTANCE_KEY:+-$AIUR_INSTANCE_KEY}@127.0.0.1}"
|
|
295
|
-
|
|
296
|
-
export AIUR_BG_STATE_DIR AIUR_COOKIE_FILE AIUR_SESSION_PREFIX \
|
|
297
|
-
AIUR_PROFILES_FILE AIUR_RELEASE_NODE AIUR_INSTANCE_KEY \
|
|
298
|
-
AIUR_PROJECT_ROOT AIUR_PROJECT_ROOT_SOURCE
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
aiur_print_identity() {
|
|
302
|
-
aiur_resolve_identity
|
|
303
|
-
printf 'AIUR_RELEASE_DIR=%s\n' "${AIUR_RELEASE_DIR:-}"
|
|
304
|
-
printf 'AIUR_BG_STATE_DIR=%s\n' "$AIUR_BG_STATE_DIR"
|
|
305
|
-
printf 'AIUR_SESSION_PREFIX=%s\n' "$AIUR_SESSION_PREFIX"
|
|
306
|
-
printf 'AIUR_PROFILES_FILE=%s\n' "$AIUR_PROFILES_FILE"
|
|
307
|
-
printf 'AIUR_RELEASE_NODE=%s\n' "$AIUR_RELEASE_NODE"
|
|
308
|
-
printf 'AIUR_INSTANCE_KEY=%s\n' "$AIUR_INSTANCE_KEY"
|
|
309
|
-
printf 'AIUR_PROJECT_ROOT=%s\n' "$AIUR_PROJECT_ROOT"
|
|
310
|
-
printf 'AIUR_PROJECT_ROOT_SOURCE=%s\n' "$AIUR_PROJECT_ROOT_SOURCE"
|
|
311
|
-
printf 'AIUR_COOKIE_FILE=%s\n' "$AIUR_COOKIE_FILE"
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
# --- BEAM distribution (cookie + named node) ---------------------------------
|
|
315
|
-
|
|
316
|
-
ensure_bg_state_dir() {
|
|
317
|
-
aiur_resolve_identity
|
|
318
|
-
mkdir -p "$AIUR_BG_STATE_DIR"
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
ensure_erlang_cookie() {
|
|
322
|
-
aiur_resolve_identity
|
|
323
|
-
ensure_bg_state_dir
|
|
324
|
-
|
|
325
|
-
local cookie_file="$AIUR_COOKIE_FILE"
|
|
326
|
-
|
|
327
|
-
if [ ! -f "$cookie_file" ]; then
|
|
328
|
-
(
|
|
329
|
-
umask 0177
|
|
330
|
-
tmp_file="$(mktemp "$AIUR_BG_STATE_DIR/cookie.XXXXXX")"
|
|
331
|
-
head -c 32 /dev/urandom | base64 | tr -d '\n=+/' | head -c 32 >"$tmp_file"
|
|
332
|
-
mv "$tmp_file" "$cookie_file"
|
|
333
|
-
)
|
|
334
|
-
chmod 0400 "$cookie_file"
|
|
335
|
-
fi
|
|
336
|
-
|
|
337
|
-
[ -r "$cookie_file" ] || die "$cookie_file is not readable"
|
|
338
|
-
|
|
339
|
-
if [ "$(stat -c '%U' "$cookie_file" 2>/dev/null || stat -f '%Su' "$cookie_file")" != "$USER" ]; then
|
|
340
|
-
die "$cookie_file is not owned by $USER"
|
|
341
|
-
fi
|
|
342
|
-
|
|
343
|
-
local size
|
|
344
|
-
size="$(wc -c <"$cookie_file" | tr -d ' ')"
|
|
345
|
-
[ "$size" -ge 16 ] || die "$cookie_file is shorter than 16 bytes"
|
|
346
|
-
|
|
347
|
-
printf '%s' "$cookie_file"
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
prepare_distribution() {
|
|
351
|
-
aiur_resolve_identity
|
|
352
|
-
|
|
353
|
-
local cookie_file
|
|
354
|
-
cookie_file="$(ensure_erlang_cookie)" || return 1
|
|
355
|
-
|
|
356
|
-
local cookie
|
|
357
|
-
cookie="$(cat "$cookie_file")"
|
|
358
|
-
|
|
359
|
-
export RELEASE_DISTRIBUTION="name"
|
|
360
|
-
export RELEASE_NODE="$AIUR_RELEASE_NODE"
|
|
361
|
-
export RELEASE_COOKIE="$cookie"
|
|
362
|
-
# Pin the distribution listener to 127.0.0.1 too, matching the node-name IP.
|
|
363
|
-
# {127,0,0,1} is the Erlang tuple — unquoted because bash inside the
|
|
364
|
-
# surrounding double quotes does not brace-expand it.
|
|
365
|
-
export ERL_AFLAGS=" -proto_dist inet_tcp -kernel inet_dist_use_interface {127,0,0,1}"
|
|
366
|
-
export ERL_EPMD_ADDRESS="127.0.0.1"
|
|
367
|
-
export AIUR_NODE="$RELEASE_NODE"
|
|
368
|
-
export AIUR_ERLANG_COOKIE="$RELEASE_COOKIE"
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
# --- release resolution ------------------------------------------------------
|
|
372
|
-
|
|
373
|
-
release_dir=""
|
|
374
|
-
vsn_dir=""
|
|
375
|
-
release_bin=""
|
|
376
|
-
|
|
377
|
-
control_release_retry() {
|
|
378
|
-
[ -n "${AIUR_CONTROL_RELEASE_RETRY_SIGNAL:-}" ] && : >"$AIUR_CONTROL_RELEASE_RETRY_SIGNAL"
|
|
379
|
-
return 75
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
resolve_release() {
|
|
383
|
-
release_dir="${AIUR_RELEASE_DIR:-}"
|
|
384
|
-
[ -n "$release_dir" ] || die "AIUR_RELEASE_DIR is not set; the engine must be invoked via the aiur or aiurdev wrapper"
|
|
385
|
-
if [ ! -d "$release_dir" ]; then
|
|
386
|
-
# aiurdev uses EX_TEMPFAIL to wait out an in-place dev rebuild and retry
|
|
387
|
-
# exactly once. Product launches keep the existing fatal diagnostics.
|
|
388
|
-
if [ "${AIUR_CONTROL_RELEASE_RETRYABLE:-0}" = "1" ]; then
|
|
389
|
-
control_release_retry
|
|
390
|
-
return $?
|
|
391
|
-
fi
|
|
392
|
-
die "AIUR_RELEASE_DIR does not exist: $release_dir"
|
|
393
|
-
fi
|
|
394
|
-
|
|
395
|
-
local release_vsn
|
|
396
|
-
release_vsn="$(cut -d' ' -f2 "$release_dir/releases/start_erl.data" 2>/dev/null || true)"
|
|
397
|
-
vsn_dir="$release_dir/releases/$release_vsn"
|
|
398
|
-
release_bin="$release_dir/bin/aiur"
|
|
399
|
-
if [ -z "$release_vsn" ] || [ ! -x "$release_bin" ] || [ ! -x "$vsn_dir/elixir" ]; then
|
|
400
|
-
if [ "${AIUR_CONTROL_RELEASE_RETRYABLE:-0}" = "1" ]; then
|
|
401
|
-
control_release_retry
|
|
402
|
-
return $?
|
|
403
|
-
fi
|
|
404
|
-
die "release elixir launcher not found at $vsn_dir/elixir"
|
|
405
|
-
fi
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
# --- argv round-trip (System.argv is empty under `elixir --eval`) -------------
|
|
409
|
-
|
|
410
|
-
argv_file=""
|
|
411
|
-
init_argv_file() {
|
|
412
|
-
argv_file="$(mktemp "${TMPDIR:-/tmp}/aiur-argv.XXXXXX")"
|
|
413
|
-
: >"$argv_file"
|
|
414
|
-
}
|
|
415
|
-
write_argv() {
|
|
416
|
-
local a
|
|
417
|
-
for a in "$@"; do printf '%s\n' "$a" >>"$argv_file"; done
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
release_cmd=()
|
|
421
|
-
build_release_cmd() {
|
|
422
|
-
release_cmd=(
|
|
423
|
-
"$vsn_dir/elixir"
|
|
424
|
-
--cookie "$RELEASE_COOKIE"
|
|
425
|
-
--name "$RELEASE_NODE"
|
|
426
|
-
--erl-config "$vsn_dir/sys"
|
|
427
|
-
--boot "$vsn_dir/start_clean"
|
|
428
|
-
--boot-var RELEASE_LIB "$release_dir/lib"
|
|
429
|
-
--vm-args "$vsn_dir/vm.args"
|
|
430
|
-
--eval "Aiur.CLI.main(Aiur.CLI.argv_from_file())"
|
|
431
|
-
)
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
# Distribution-free boot for the `init` wizard: same interactive `--eval` form
|
|
435
|
-
# (so the wizard's prompts receive keystrokes — `bin/aiur eval` is -noinput),
|
|
436
|
-
# but with no --name/--cookie since the wizard makes no RPC calls.
|
|
437
|
-
build_init_cmd() {
|
|
438
|
-
release_cmd=(
|
|
439
|
-
"$vsn_dir/elixir"
|
|
440
|
-
--erl-config "$vsn_dir/sys"
|
|
441
|
-
--boot "$vsn_dir/start_clean"
|
|
442
|
-
--boot-var RELEASE_LIB "$release_dir/lib"
|
|
443
|
-
--vm-args "$vsn_dir/vm.args"
|
|
444
|
-
--eval "Aiur.CLI.main(Aiur.CLI.argv_from_file())"
|
|
445
|
-
)
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
usage() {
|
|
449
|
-
cat <<'EOF'
|
|
450
|
-
Usage: aiur [--interactive] [--no-dashboard] [--executor] [--pause] [--max-agents <n>] [--logs-root <path>] [--port <port>] [--host <host>] [config-path]
|
|
451
|
-
aiur run [--bg] [--no-dashboard] [--executor] [--debug] explicit launch form (foreground unless --bg)
|
|
452
|
-
aiur init [--force] scaffold .aiur/config (interactive setup wizard)
|
|
453
|
-
aiur --bg [--no-dashboard] [--executor] [--debug] start detached; dashboard on unless suppressed
|
|
454
|
-
aiur stop stop the running session
|
|
455
|
-
aiur restart [--no-build] [run flags] stop, refresh the build, start again (detached)
|
|
456
|
-
aiur status show agent status
|
|
457
|
-
aiur agents show each agent's state + current activity
|
|
458
|
-
aiur commands [<decision-id>] [--filter all|open|blocking|resolved] [--blocking] [--ticket <id>] [--search <text>] [--cursor <cursor>] [--limit <n>] [--json]
|
|
459
|
-
aiur executor-answer <decision-id> --expected-version <n> (--option <id>|--custom-response <text>) --rationale <text> --idempotency-key <key> [--supersede] [--executor-id <id>]
|
|
460
|
-
aiur executor-escalate <decision-id> --expected-version <n> --reason <text> [--executor-id <id>]
|
|
461
|
-
aiur executor-moot <decision-id> --expected-version <n> --reason-class <class> [--reason <text>] [--executor-id <id>]
|
|
462
|
-
aiur units [--scope live|unfinished|all|none] [--condition active|alert|paused|queued|finished]... [--format auto|table|records] [--json]
|
|
463
|
-
aiur build-orders [<root>] [--json] show the Build Order catalog or one root
|
|
464
|
-
aiur analytics [--range run|full] [--since <ISO-8601>] [--until <ISO-8601>] [--build-order <id>] [--json]
|
|
465
|
-
aiur github-cost [--budget graphql|core|all] [--format auto|table|records] [--json] rank GitHub API spend by call site
|
|
466
|
-
aiur github-usage [--json] per-actor (daemon vs agent) GitHub usage and ceilings
|
|
467
|
-
aiur alerts [--needs-attention] show structured alert feed
|
|
468
|
-
aiur watch [--full|--changes] [--interval <secs>] server-side status board
|
|
469
|
-
aiur executor-listen [--topic <pattern>] stream Executor events as JSON lines
|
|
470
|
-
aiur executor-wait [--timeout <seconds>] [--json] block until Executor work arrives
|
|
471
|
-
aiur executor-fast-forward <wake-id> [--as <id>] acknowledge an externally covered wake prefix
|
|
472
|
-
aiur executor-emit <topic> --payload <json> publish an Executor event
|
|
473
|
-
aiur executor-subscribe|executor-unsubscribe <pattern>
|
|
474
|
-
aiur executor-subscriptions list persistent Executor bindings
|
|
475
|
-
aiur executor-roster [--json] list Executor consumers with their liveness evidence
|
|
476
|
-
aiur executor-claim [--as <id>] claim the wake stream, or refuse and name the live owner
|
|
477
|
-
aiur executor-release [--as <id>] give up this consumer's claim
|
|
478
|
-
aiur executor-revoke <consumer-id> operator-only revoke of a live owner's claim
|
|
479
|
-
aiur set max-agents <n> change the concurrent-agent cap at runtime
|
|
480
|
-
aiur upgrade [--force] install the newer aiur-cli on your channel
|
|
481
|
-
aiur pause | resume flip the global pause switch (whole daemon)
|
|
482
|
-
aiur pause <ids|--all> | resume <ids|--all> per-agent pause/resume
|
|
483
|
-
aiur message <id> [--message-id ID] <text> send Executor text to a running agent
|
|
484
|
-
aiur --todo <ids...> [--only] queue tickets; optionally dequeue all other pending tickets
|
|
485
|
-
aiur findings [--unfiled] [--slugs] [--scope aiur|repo] inspect host-local findings
|
|
486
|
-
aiur findings --record <json> --repo <owner/repo> append one validated finding
|
|
487
|
-
aiur findings --digest [--scope aiur|repo] generate the promoted Markdown digest
|
|
488
|
-
aiur ask <title> [--body <text>|--body-file <path>] [--urgency low|normal|high] [--blocking]
|
|
489
|
-
aiur ask --done <id> [--note <text>] create or resolve an operator request
|
|
490
|
-
aiur asks [--open|--all] [--json] inspect current-repository operator requests
|
|
491
|
-
aiur cleanup-stale [--dry-run] list/reap stale manual-smoke leftovers
|
|
492
|
-
aiur --version
|
|
493
|
-
|
|
494
|
-
Bare aiur: start or attach to this directory's interactive session.
|
|
495
|
-
EOF
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
# --- one-shot: --version (no tmux) -------------------------------------------
|
|
499
|
-
|
|
500
|
-
run_version() {
|
|
501
|
-
resolve_release
|
|
502
|
-
AIUR_CLI_VERSION="$(cli_package_version || true)"
|
|
503
|
-
export AIUR_CLI_VERSION
|
|
504
|
-
# Distribution-free (like init): printing the version is a compile-time
|
|
505
|
-
# constant, so never claim the node name — otherwise `aiur --version` fails
|
|
506
|
-
# whenever an aiur session is already running.
|
|
507
|
-
build_init_cmd
|
|
508
|
-
init_argv_file
|
|
509
|
-
write_argv "$@"
|
|
510
|
-
export AIUR_ARGV_FILE="$argv_file"
|
|
511
|
-
exec "${release_cmd[@]}"
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
# --- one-shot: init (interactive wizard, distribution-free, no tmux) ----------
|
|
515
|
-
|
|
516
|
-
run_init() {
|
|
517
|
-
resolve_release
|
|
518
|
-
build_init_cmd
|
|
519
|
-
init_argv_file
|
|
520
|
-
write_argv "$@"
|
|
521
|
-
export AIUR_ARGV_FILE="$argv_file"
|
|
522
|
-
exec "${release_cmd[@]}"
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
# --- one-shot: --todo (control RPC; requires a running daemon) ----------------
|
|
526
|
-
|
|
527
|
-
parsed_todo_only=0
|
|
528
|
-
|
|
529
|
-
parse_todo_args() {
|
|
530
|
-
parsed_targets=()
|
|
531
|
-
parsed_todo_only=0
|
|
532
|
-
|
|
533
|
-
local saw_todo=0 raw part parts
|
|
534
|
-
for raw in "$@"; do
|
|
535
|
-
case "$raw" in
|
|
536
|
-
--todo)
|
|
537
|
-
[ "$saw_todo" -eq 0 ] || return 1
|
|
538
|
-
saw_todo=1
|
|
539
|
-
;;
|
|
540
|
-
--only)
|
|
541
|
-
parsed_todo_only=1
|
|
542
|
-
;;
|
|
543
|
-
-*)
|
|
544
|
-
return 1
|
|
545
|
-
;;
|
|
546
|
-
*)
|
|
547
|
-
IFS=',' read -ra parts <<<"$raw"
|
|
548
|
-
for part in "${parts[@]}"; do
|
|
549
|
-
part="$(trim "$part")"
|
|
550
|
-
if [ -z "$part" ] || [[ ! "$part" =~ ^[0-9]+$ ]]; then return 1; fi
|
|
551
|
-
while [ "${#part}" -gt 1 ] && [ "${part#0}" != "$part" ]; do
|
|
552
|
-
part="${part#0}"
|
|
553
|
-
done
|
|
554
|
-
parsed_targets+=("$part")
|
|
555
|
-
done
|
|
556
|
-
;;
|
|
557
|
-
esac
|
|
558
|
-
done
|
|
559
|
-
|
|
560
|
-
[ "$saw_todo" -eq 1 ] && [ "${#parsed_targets[@]}" -gt 0 ]
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
run_todo() {
|
|
564
|
-
if ! parse_todo_args "$@"; then
|
|
565
|
-
echo "aiur: --todo expects one or more numeric issue IDs, optionally followed by --only" >&2
|
|
566
|
-
exit 64
|
|
567
|
-
fi
|
|
568
|
-
|
|
569
|
-
local only_arg="false"
|
|
570
|
-
[ "$parsed_todo_only" -eq 1 ] && only_arg="true"
|
|
571
|
-
run_control_rpc "Aiur.AgentControlCLI.todo($(elixir_list_literal "${parsed_targets[@]}"), only: $only_arg, emit_exit_marker: true)"
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
# --- one-shot: findings (distribution-free, no daemon/tmux) -------------------
|
|
575
|
-
|
|
576
|
-
run_findings() {
|
|
577
|
-
run_init "$@"
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
# --- one-shot: ask / asks (distribution-free, no daemon/tmux) ----------------
|
|
581
|
-
|
|
582
|
-
run_asks() {
|
|
583
|
-
run_init "$@"
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
# --- interactive / background run -------------------------------------------
|
|
587
|
-
#
|
|
588
|
-
# mode=foreground attaches the UI and tears down on exit; mode=background leaves
|
|
589
|
-
# the detached tmux session running and returns.
|
|
590
|
-
|
|
591
|
-
# Dotenv precedence: shell exports win, then ./.env, then ~/.aiur/.env. Each
|
|
592
|
-
# file only fills names that are still unset, so the repo-local file (the more
|
|
593
|
-
# specific scope) is read first and the machine-wide file only fills its gaps.
|
|
594
|
-
# A blank value is a placeholder, not a setting: `aiur init` scaffolds
|
|
595
|
-
# `GITHUB_TOKEN=` and `.env.example` renders every name blank, so a blank line
|
|
596
|
-
# neither exports nor shadows anything.
|
|
597
|
-
#
|
|
598
|
-
# GitHub credentials are one group. A repo `.env` that sets any member has
|
|
599
|
-
# chosen that repository's auth mode, so none of the global file's members may
|
|
600
|
-
# leak in beside it: a global GITHUB_APP_* triple filling the gaps around a
|
|
601
|
-
# repo-local GITHUB_TOKEN would outrank that token and force the daemon onto an
|
|
602
|
-
# App that is not installed on the repo (#2638).
|
|
603
|
-
GITHUB_CREDENTIAL_ENV_NAMES="GITHUB_TOKEN GITHUB_APP_ID GITHUB_APP_INSTALLATION_ID GITHUB_APP_PRIVATE_KEY_PATH GITHUB_APP_PRIVATE_KEY"
|
|
604
|
-
|
|
605
|
-
load_dotenv() {
|
|
606
|
-
local global_skip=""
|
|
607
|
-
if dotenv_file_sets_github_credential ".env"; then
|
|
608
|
-
global_skip="github_credentials"
|
|
609
|
-
fi
|
|
610
|
-
load_dotenv_file ".env"
|
|
611
|
-
load_dotenv_file "$HOME/.aiur/.env" "$global_skip"
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
github_credential_env_name() {
|
|
615
|
-
case " $GITHUB_CREDENTIAL_ENV_NAMES " in
|
|
616
|
-
*" $1 "*) return 0 ;;
|
|
617
|
-
esac
|
|
618
|
-
return 1
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
# Parses one dotenv line into DOTENV_KEY / DOTENV_VAL. Returns 1 for blank,
|
|
622
|
-
# comment, malformed, and blank-valued lines so callers skip them. Shared by
|
|
623
|
-
# the loader and the credential-group probe so both agree on what counts.
|
|
624
|
-
parse_dotenv_line() {
|
|
625
|
-
local line="$1" key val
|
|
626
|
-
DOTENV_KEY="" DOTENV_VAL=""
|
|
627
|
-
line="${line#"${line%%[![:space:]]*}"}"
|
|
628
|
-
case "$line" in '' | '#'*) return 1 ;; esac
|
|
629
|
-
[ "${line#*=}" = "$line" ] && return 1
|
|
630
|
-
key="${line%%=*}"
|
|
631
|
-
key="${key%"${key##*[![:space:]]}"}"
|
|
632
|
-
case "$key" in '' | *[!A-Za-z0-9_]*) return 1 ;; esac
|
|
633
|
-
val="${line#*=}"
|
|
634
|
-
val="${val#"${val%%[![:space:]]*}"}"
|
|
635
|
-
case "$val" in
|
|
636
|
-
\"*\") val="${val#\"}" && val="${val%\"}" ;;
|
|
637
|
-
\'*\') val="${val#\'}" && val="${val%\'}" ;;
|
|
638
|
-
esac
|
|
639
|
-
[ -n "$val" ] || return 1
|
|
640
|
-
DOTENV_KEY="$key"
|
|
641
|
-
DOTENV_VAL="$val"
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
# True when the file sets any GitHub credential name to a non-blank value.
|
|
645
|
-
dotenv_file_sets_github_credential() {
|
|
646
|
-
local file="$1" line
|
|
647
|
-
[ -f "$file" ] || return 1
|
|
648
|
-
while IFS= read -r line || [ -n "$line" ]; do
|
|
649
|
-
parse_dotenv_line "$line" || continue
|
|
650
|
-
if github_credential_env_name "$DOTENV_KEY"; then
|
|
651
|
-
return 0
|
|
652
|
-
fi
|
|
653
|
-
done <"$file"
|
|
654
|
-
return 1
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
# Usage: load_dotenv_file <file> [github_credentials]
|
|
658
|
-
# The optional second argument skips the GitHub credential group.
|
|
659
|
-
load_dotenv_file() {
|
|
660
|
-
local file="$1" skip="${2:-}" line
|
|
661
|
-
[ -f "$file" ] || return 0
|
|
662
|
-
while IFS= read -r line || [ -n "$line" ]; do
|
|
663
|
-
parse_dotenv_line "$line" || continue
|
|
664
|
-
if [ "$skip" = "github_credentials" ] && github_credential_env_name "$DOTENV_KEY"; then
|
|
665
|
-
continue
|
|
666
|
-
fi
|
|
667
|
-
[ -n "${!DOTENV_KEY+x}" ] && continue
|
|
668
|
-
export "$DOTENV_KEY=$DOTENV_VAL"
|
|
669
|
-
done <"$file"
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
# The readiness token grants the one-shot `aiur init` assessment access that a
|
|
673
|
-
# normal daemon and its child agents must never inherit. Keep dotenv loading
|
|
674
|
-
# generic, then remove this run-only secret before any session process starts.
|
|
675
|
-
scrub_run_only_env() {
|
|
676
|
-
unset AIUR_CI_READINESS_TOKEN
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
run_argv=()
|
|
680
|
-
# Default dashboard bind host. Prefer this machine's Tailscale IPv4 so the
|
|
681
|
-
# dashboard is reachable across the tailnet by default (when config omits it);
|
|
682
|
-
# fall back to loopback when Tailscale is absent, or when dashboard credentials
|
|
683
|
-
# are unset (a non-loopback bind requires them, so we stay on loopback rather
|
|
684
|
-
# than refuse to start). The BEAM applies this below an explicit `server.host`,
|
|
685
|
-
# while an explicit `--host` remains the highest-precedence value.
|
|
686
|
-
default_dashboard_host() {
|
|
687
|
-
local ip=""
|
|
688
|
-
if command -v tailscale >/dev/null 2>&1; then
|
|
689
|
-
ip="$(tailscale ip -4 2>/dev/null | grep -m1 -E '^100\.[0-9]+\.[0-9]+\.[0-9]+$' || true)"
|
|
690
|
-
fi
|
|
691
|
-
if [ -n "$ip" ] && [ -n "${AIUR_DASHBOARD_USERNAME:-}" ] && [ -n "${AIUR_DASHBOARD_PASSWORD:-}" ]; then
|
|
692
|
-
printf '%s' "$ip"
|
|
693
|
-
else
|
|
694
|
-
printf '127.0.0.1'
|
|
695
|
-
fi
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
build_run_argv() {
|
|
699
|
-
local mode="$1"
|
|
700
|
-
shift
|
|
701
|
-
|
|
702
|
-
local has_interactive=0 has_headless=0 has_ack=0 arg
|
|
703
|
-
for arg in "$@"; do
|
|
704
|
-
case "$arg" in
|
|
705
|
-
--interactive) has_interactive=1 ;;
|
|
706
|
-
--headless) has_headless=1 ;;
|
|
707
|
-
--i-understand-that-this-will-be-running-without-the-usual-guardrails) has_ack=1 ;;
|
|
708
|
-
esac
|
|
709
|
-
done
|
|
710
|
-
|
|
711
|
-
local injected=()
|
|
712
|
-
if [ "$mode" = "background" ] && [ "$has_interactive" -eq 0 ]; then
|
|
713
|
-
[ "$has_headless" -eq 1 ] || injected+=(--headless)
|
|
714
|
-
else
|
|
715
|
-
[ "$has_interactive" -eq 1 ] || injected+=(--interactive)
|
|
716
|
-
fi
|
|
717
|
-
[ "$has_ack" -eq 1 ] || injected+=(--i-understand-that-this-will-be-running-without-the-usual-guardrails)
|
|
718
|
-
|
|
719
|
-
run_argv=("${injected[@]}" "$@")
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
run_session() {
|
|
723
|
-
local mode="$1"
|
|
724
|
-
shift
|
|
725
|
-
|
|
726
|
-
# --debug is an engine-level convenience on the run path: turn on verbose
|
|
727
|
-
# logging (AIUR_DEBUG) and strip the flag before the release parses argv.
|
|
728
|
-
local run_args=() run_arg
|
|
729
|
-
for run_arg in "$@"; do
|
|
730
|
-
case "$run_arg" in
|
|
731
|
-
--debug) export AIUR_DEBUG=1 ;;
|
|
732
|
-
*) run_args+=("$run_arg") ;;
|
|
733
|
-
esac
|
|
734
|
-
done
|
|
735
|
-
if [ "${#run_args[@]}" -gt 0 ]; then set -- "${run_args[@]}"; else set --; fi
|
|
736
|
-
|
|
737
|
-
resolve_release
|
|
738
|
-
|
|
739
|
-
local tmux_bin
|
|
740
|
-
tmux_bin="$(command -v tmux || true)"
|
|
741
|
-
[ -n "$tmux_bin" ] || die "tmux is required to run aiur; install tmux and retry"
|
|
742
|
-
|
|
743
|
-
aiur_resolve_identity
|
|
744
|
-
prepare_distribution
|
|
745
|
-
|
|
746
|
-
local session="${AIUR_SESSION_PREFIX}-${USER:-user}${AIUR_INSTANCE_KEY:+-$AIUR_INSTANCE_KEY}-default"
|
|
747
|
-
local socket="${AIUR_SESSION_PREFIX}-${USER:-user}${AIUR_INSTANCE_KEY:+-$AIUR_INSTANCE_KEY}"
|
|
748
|
-
local conf launch_lock
|
|
749
|
-
conf="$(resolve_tmux_conf)"
|
|
750
|
-
[ -f "$conf" ] || die "tmux conf not found at $conf"
|
|
751
|
-
launch_lock="$(aiur_launch_lock_path)"
|
|
752
|
-
acquire_aiur_launch_lock "$launch_lock" || return 1
|
|
753
|
-
|
|
754
|
-
# An invocation targeting an existing directory session resolves here before
|
|
755
|
-
# fresh-start credentials, argv files, run logs, watchdogs, or teardown traps.
|
|
756
|
-
# Foreground attaches; background exits with explicit attach guidance.
|
|
757
|
-
if "$tmux_bin" -L "$socket" -f "$conf" has-session -t "$session" 2>/dev/null; then
|
|
758
|
-
if [ "$(probe_control_liveness)" = "up" ]; then
|
|
759
|
-
# Preserve the live launch's record and workspace-root handoff. Only
|
|
760
|
-
# backfill old/missing records, without pointing them at this attach
|
|
761
|
-
# invocation's transient state.
|
|
762
|
-
AIUR_WORKSPACE_ROOT_FILE="" write_aiur_instance_record "$session" "$socket" if-absent
|
|
763
|
-
|
|
764
|
-
if [ "$mode" = "background" ]; then
|
|
765
|
-
echo "aiur is already running in the background (tmux session ${session})." >&2
|
|
766
|
-
echo "Attach with: aiur" >&2
|
|
767
|
-
echo "Use: aiur status # inspect agents" >&2
|
|
768
|
-
echo "Use: aiur stop # stop it before starting a fresh session" >&2
|
|
769
|
-
release_aiur_launch_lock "$launch_lock"
|
|
770
|
-
return 0
|
|
771
|
-
fi
|
|
772
|
-
|
|
773
|
-
echo "aiur is already running; attaching to the running session ${session}." >&2
|
|
774
|
-
release_aiur_launch_lock "$launch_lock"
|
|
775
|
-
attach_tmux_session "$tmux_bin" "$socket" "$conf" "$session"
|
|
776
|
-
return $?
|
|
777
|
-
fi
|
|
778
|
-
|
|
779
|
-
# A tmux holder can exist while a healthy BEAM is still booting or while
|
|
780
|
-
# epmd itself is temporarily unreachable. Never turn one failed control
|
|
781
|
-
# probe into destructive cleanup unless distribution confirms the keyed
|
|
782
|
-
# node is absent.
|
|
783
|
-
case "$(probe_node_liveness)" in
|
|
784
|
-
down)
|
|
785
|
-
if ! confirm_stale_tmux_session "$tmux_bin" "$socket" "$conf" "$session"; then
|
|
786
|
-
echo "aiur found session ${session}, but it became live while checking stale state; leaving it intact." >&2
|
|
787
|
-
echo "Retry: aiur" >&2
|
|
788
|
-
release_aiur_launch_lock "$launch_lock"
|
|
789
|
-
return 1
|
|
790
|
-
fi
|
|
791
|
-
echo "aiur found stale tmux session ${session}; cleaning it up before restart" >&2
|
|
792
|
-
reap_aiur_agents "$socket" ""
|
|
793
|
-
kill_beams_matching "-name ${AIUR_RELEASE_NODE}"
|
|
794
|
-
;;
|
|
795
|
-
up | unknown)
|
|
796
|
-
echo "aiur found session ${session}, but its control plane is not ready; leaving the running session intact." >&2
|
|
797
|
-
echo "Retry: aiur" >&2
|
|
798
|
-
release_aiur_launch_lock "$launch_lock"
|
|
799
|
-
return 1
|
|
800
|
-
;;
|
|
801
|
-
esac
|
|
802
|
-
fi
|
|
803
|
-
|
|
804
|
-
# Pick up GITHUB_TOKEN / dashboard creds the wizard wrote to ./.env so the
|
|
805
|
-
# running tracker can authenticate. Shell exports still take precedence.
|
|
806
|
-
load_dotenv
|
|
807
|
-
scrub_run_only_env
|
|
808
|
-
export AIUR_DEFAULT_DASHBOARD_HOST="$(default_dashboard_host)"
|
|
809
|
-
|
|
810
|
-
# The BEAM lives in tmux, but a fresh foreground run belongs to this shell:
|
|
811
|
-
# it waits on the UI attach and owns the teardown trap. Hand its pid to the
|
|
812
|
-
# pane watchdog so a hard-killed launcher cannot leave agents running.
|
|
813
|
-
# A detached run has no such owner; discard any inherited stale value.
|
|
814
|
-
if [ "$mode" = "foreground" ]; then
|
|
815
|
-
export AIUR_LAUNCHER_PID="$$"
|
|
816
|
-
else
|
|
817
|
-
unset AIUR_LAUNCHER_PID
|
|
818
|
-
fi
|
|
819
|
-
|
|
820
|
-
# The daemon's `Aiur.Upgrade` check uses the CLI package version (not the mix
|
|
821
|
-
# version) as the "installed" version, so an npm install's notice names what
|
|
822
|
-
# the user actually has. `run_version` sets the same var for `--version`.
|
|
823
|
-
AIUR_CLI_VERSION="$(cli_package_version || true)"
|
|
824
|
-
export AIUR_CLI_VERSION
|
|
825
|
-
|
|
826
|
-
init_argv_file
|
|
827
|
-
|
|
828
|
-
# Supply the flags a bare `aiur` needs: UI mode and the no-guardrails ack.
|
|
829
|
-
# The exported dashboard host fills only a missing config value; an explicit
|
|
830
|
-
# server.host or --host remains authoritative. Foreground runs are
|
|
831
|
-
# interactive; `--bg` runs headless (no panes/chat backfill) and is driven
|
|
832
|
-
# over the control RPC (status/agents/message/pause/set). Dashboard binding is
|
|
833
|
-
# independent: it remains enabled in either mode unless `--no-dashboard` is
|
|
834
|
-
# supplied. `aiur --bg --interactive` opts back into the full terminal stack
|
|
835
|
-
# for an attachable background session.
|
|
836
|
-
build_run_argv "$mode" "$@"
|
|
837
|
-
local no_dashboard=0
|
|
838
|
-
for run_arg in "${run_argv[@]}"; do
|
|
839
|
-
[ "$run_arg" = "--no-dashboard" ] && no_dashboard=1
|
|
840
|
-
done
|
|
841
|
-
write_argv "${run_argv[@]}"
|
|
842
|
-
export AIUR_ARGV_FILE="$argv_file"
|
|
843
|
-
|
|
844
|
-
build_release_cmd
|
|
845
|
-
|
|
846
|
-
# Force +fnu when no locale is set so the BEAM does not mangle non-ASCII paths.
|
|
847
|
-
if [ -z "${LANG:-}" ] && [ -z "${LC_ALL:-}" ] && [ -z "${LC_CTYPE:-}" ]; then
|
|
848
|
-
export ELIXIR_ERL_OPTIONS="${ELIXIR_ERL_OPTIONS:-} +fnu"
|
|
849
|
-
fi
|
|
850
|
-
|
|
851
|
-
preflight_stale_manual_smoke
|
|
852
|
-
|
|
853
|
-
mkdir -p "$AIUR_BG_STATE_DIR"
|
|
854
|
-
printf '%s\n' "$session" >"$AIUR_BG_STATE_DIR/state"
|
|
855
|
-
export AIUR_TMUX_SESSION="$session"
|
|
856
|
-
export AIUR_TMUX_SOCKET="$socket"
|
|
857
|
-
export AIUR_TMUX_CONF="$conf"
|
|
858
|
-
export AIUR_BIN="${BASH_SOURCE[0]}"
|
|
859
|
-
|
|
860
|
-
local session_root="${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}"
|
|
861
|
-
export AIUR_SESSION_TMPFILE="${session_root}/aiur-${$}-sessions"
|
|
862
|
-
: >"$AIUR_SESSION_TMPFILE"
|
|
863
|
-
|
|
864
|
-
# Agent pidfile: the BEAM appends one line per spawned agent (pane or headless
|
|
865
|
-
# os_pid) via Aiur.ProcessReaper. The BEAM-death watchdog and session_cleanup
|
|
866
|
-
# reap from it after the BEAM is gone — a crashed BEAM can kill nothing itself.
|
|
867
|
-
export AIUR_AGENT_TMPFILE="${session_root}/aiur-${$}-agents"
|
|
868
|
-
: >"$AIUR_AGENT_TMPFILE"
|
|
869
|
-
|
|
870
|
-
# Workspace-root handoff for the shell cwd-sweep backstop. The BEAM writes the
|
|
871
|
-
# resolved Aiur.Config.workspace_root() here after config loads.
|
|
872
|
-
export AIUR_WORKSPACE_ROOT_FILE="${session_root}/aiur-${$}-workspace-root"
|
|
873
|
-
: >"$AIUR_WORKSPACE_ROOT_FILE"
|
|
874
|
-
|
|
875
|
-
# Background runs persist under a known run log dir so (a) the BEAM-death
|
|
876
|
-
# watchdog, which outlives the BEAM, can drop a crash record next to aiur.log,
|
|
877
|
-
# and (b) the post-start boot capture is not lost to /tmp when the launcher
|
|
878
|
-
# removes its startup tempfile. Minting it here and exporting it makes the
|
|
879
|
-
# shell and the BEAM (Aiur.LogFile honors AIUR_LOGS_ROOT) agree on one dir.
|
|
880
|
-
if [ "$mode" = "background" ] && [ -z "${AIUR_LOGS_ROOT:-}" ]; then
|
|
881
|
-
AIUR_LOGS_ROOT="$(printf '%s/%s-%s' "$HOME/.aiur/logs" "$(date -u +%Y%m%dT%H%M%SZ)" "$$")"
|
|
882
|
-
export AIUR_LOGS_ROOT
|
|
883
|
-
fi
|
|
884
|
-
|
|
885
|
-
# Capture an erl_crash.dump on daemon BEAM death (#852) so a crash under load
|
|
886
|
-
# is diagnosable instead of vanishing. Written next to the run's aiur.log so it
|
|
887
|
-
# survives the launcher's tempfile cleanup; ERL_CRASH_DUMP_SECONDS bounds the
|
|
888
|
-
# write so a wedged BEAM can't hang the dump indefinitely. Nothing in the
|
|
889
|
-
# release boot disables dumps, and an Executor override of either var is kept.
|
|
890
|
-
# Requires a durable logs root (background run or agent IR sandbox).
|
|
891
|
-
if [ -n "${AIUR_LOGS_ROOT:-}" ]; then
|
|
892
|
-
export ERL_CRASH_DUMP="${ERL_CRASH_DUMP:-$AIUR_LOGS_ROOT/erl_crash.dump}"
|
|
893
|
-
export ERL_CRASH_DUMP_SECONDS="${ERL_CRASH_DUMP_SECONDS:-30}"
|
|
894
|
-
fi
|
|
895
|
-
|
|
896
|
-
# Crash recording needs two launcher/BEAM handoffs: the canonical alert
|
|
897
|
-
# ledger path and the launch-time identity of the configured dump path. Keep
|
|
898
|
-
# these for foreground launches too: their external watchdog is also the only
|
|
899
|
-
# process left to surface a dump after an unexpected daemon death.
|
|
900
|
-
export AIUR_ALERT_LEDGER_PATH_FILE=""
|
|
901
|
-
local crash_dump_baseline_file=""
|
|
902
|
-
if [ -n "${ERL_CRASH_DUMP:-}" ]; then
|
|
903
|
-
AIUR_ALERT_LEDGER_PATH_FILE="${session_root}/aiur-${$}-alert-ledger"
|
|
904
|
-
: >"$AIUR_ALERT_LEDGER_PATH_FILE"
|
|
905
|
-
crash_dump_baseline_file="${session_root}/aiur-${$}-crash-dump-baseline"
|
|
906
|
-
crash_dump_identity "${ERL_CRASH_DUMP:-}" >"$crash_dump_baseline_file" 2>/dev/null || : >"$crash_dump_baseline_file"
|
|
907
|
-
fi
|
|
908
|
-
|
|
909
|
-
# Capture sink for BEAM startup (and, in background mode, the whole run's
|
|
910
|
-
# boot stdout/stderr). Foreground uses a throwaway tempfile; background points
|
|
911
|
-
# at a durable file in the run log dir. The dir/file is created lazily just
|
|
912
|
-
# before launch (below) so an idempotent early-return start leaves no empty dir.
|
|
913
|
-
local startup_capture
|
|
914
|
-
if [ "$mode" = "background" ] && [ -n "${AIUR_LOGS_ROOT:-}" ]; then
|
|
915
|
-
startup_capture="$AIUR_LOGS_ROOT/log/boot.out.log"
|
|
916
|
-
else
|
|
917
|
-
startup_capture="$(mktemp "${TMPDIR:-/tmp}/aiur-startup.XXXXXX")"
|
|
918
|
-
fi
|
|
919
|
-
|
|
920
|
-
# Inner pane script: tmux's server may pre-exist and not inherit our env, so
|
|
921
|
-
# re-export every var the BEAM needs. tee preserves a startup capture.
|
|
922
|
-
local launcher
|
|
923
|
-
launcher="$(mktemp "${TMPDIR:-/tmp}/aiur-pane.XXXXXX")"
|
|
924
|
-
chmod +x "$launcher"
|
|
925
|
-
{
|
|
926
|
-
printf '#!/usr/bin/env bash\n'
|
|
927
|
-
printf 'set -o pipefail\n'
|
|
928
|
-
printf 'unset AIUR_CI_READINESS_TOKEN\n'
|
|
929
|
-
printf 'cd %q || exit 1\n' "$PWD"
|
|
930
|
-
local v
|
|
931
|
-
for v in AIUR_RELEASE_DIR AIUR_ARGV_FILE RELEASE_DISTRIBUTION RELEASE_NODE \
|
|
932
|
-
RELEASE_COOKIE ERL_AFLAGS ERL_EPMD_ADDRESS AIUR_NODE AIUR_ERLANG_COOKIE \
|
|
933
|
-
AIUR_TMUX_SESSION AIUR_TMUX_SOCKET AIUR_TMUX_CONF AIUR_BIN \
|
|
934
|
-
AIUR_SESSION_TMPFILE AIUR_AGENT_TMPFILE AIUR_WORKSPACE_ROOT_FILE AIUR_ALERT_LEDGER_PATH_FILE \
|
|
935
|
-
ELIXIR_ERL_OPTIONS AIUR_LOGS_ROOT AIUR_OPENCODE_BRIDGE_PORT AIUR_DEFAULT_DASHBOARD_HOST AIUR_DEBUG AIUR_DEV_TEST_TICKET_IDS \
|
|
936
|
-
AIUR_OPERATOR_PID AIUR_LAUNCHER_PID AIUR_NOFILE_SOFT_LIMIT ERL_CRASH_DUMP ERL_CRASH_DUMP_SECONDS \
|
|
937
|
-
AIUR_BG_STATE_DIR AIUR_CLI_VERSION; do
|
|
938
|
-
if [ -n "${!v:-}" ]; then printf 'export %s=%q\n' "$v" "${!v}"; fi
|
|
939
|
-
done
|
|
940
|
-
printf 'capture=%q\n' "$startup_capture"
|
|
941
|
-
printf '%q' "${release_cmd[0]}"
|
|
942
|
-
for arg in "${release_cmd[@]:1}"; do printf ' %q' "$arg"; done
|
|
943
|
-
printf ' 2>&1 | tee -a "$capture"\n'
|
|
944
|
-
printf 'exit ${PIPESTATUS[0]}\n'
|
|
945
|
-
} >"$launcher"
|
|
946
|
-
|
|
947
|
-
local inner_cmd
|
|
948
|
-
printf -v inner_cmd '%q; rc=$?; rm -f %q; exit $rc' "$launcher" "$launcher"
|
|
949
|
-
|
|
950
|
-
local launch_tempfiles=(
|
|
951
|
-
"$startup_capture" "$argv_file" "$launcher" "$AIUR_SESSION_TMPFILE"
|
|
952
|
-
"$AIUR_AGENT_TMPFILE" "$AIUR_WORKSPACE_ROOT_FILE"
|
|
953
|
-
)
|
|
954
|
-
[ -n "$AIUR_ALERT_LEDGER_PATH_FILE" ] && launch_tempfiles+=("$AIUR_ALERT_LEDGER_PATH_FILE")
|
|
955
|
-
[ -n "$crash_dump_baseline_file" ] && launch_tempfiles+=("$crash_dump_baseline_file")
|
|
956
|
-
|
|
957
|
-
if [ "$mode" = "foreground" ]; then
|
|
958
|
-
_session_socket="$socket" _session_name="$session" _session_conf="$conf" \
|
|
959
|
-
_session_tmpfile="$AIUR_SESSION_TMPFILE" _session_capture="$startup_capture" \
|
|
960
|
-
_session_argv="$argv_file" _session_release="$release_dir" _session_tmux="$tmux_bin" \
|
|
961
|
-
_session_node="$AIUR_RELEASE_NODE" _session_pidfile="$AIUR_AGENT_TMPFILE" \
|
|
962
|
-
_session_workspace_root_file="$AIUR_WORKSPACE_ROOT_FILE" \
|
|
963
|
-
_session_alert_ledger_path_file="$AIUR_ALERT_LEDGER_PATH_FILE" \
|
|
964
|
-
_session_crash_dump_baseline_file="$crash_dump_baseline_file" \
|
|
965
|
-
_session_launch_lock="$launch_lock"
|
|
966
|
-
install_foreground_traps
|
|
967
|
-
fi
|
|
968
|
-
|
|
969
|
-
# An orphaned BEAM (its tmux session gone) can still hold THIS instance's node
|
|
970
|
-
# name. With no live session on our (instance-keyed) socket, reap the name-holder
|
|
971
|
-
# so the launch isn't blocked by "name seems to be in use". The keyed name means
|
|
972
|
-
# this only ever reaps our own instance's orphan, never another live aiur.
|
|
973
|
-
if ! "$tmux_bin" -L "$socket" -f "$conf" has-session -t "$session" 2>/dev/null; then
|
|
974
|
-
kill_beams_matching "-name ${AIUR_RELEASE_NODE}"
|
|
975
|
-
fi
|
|
976
|
-
|
|
977
|
-
# Transition reclaim: a keyed instance also reaps a stale beam under the legacy
|
|
978
|
-
# un-keyed name (aiur-$USER@127.0.0.1) — but only when no live legacy session
|
|
979
|
-
# exists, so a pre-fix run still in progress is never killed. (Runs on every keyed
|
|
980
|
-
# launch, not latched; it's a cheap best-effort sweep guarded by has-session.)
|
|
981
|
-
if [ -n "${AIUR_INSTANCE_KEY:-}" ]; then
|
|
982
|
-
local legacy_socket="${AIUR_SESSION_PREFIX}-${USER:-user}"
|
|
983
|
-
if ! "$tmux_bin" -L "$legacy_socket" -f "$conf" has-session -t "${legacy_socket}-default" 2>/dev/null; then
|
|
984
|
-
kill_beams_matching "-name aiur-${USER}@127.0.0.1"
|
|
985
|
-
fi
|
|
986
|
-
fi
|
|
987
|
-
|
|
988
|
-
# Create the capture sink now (after the idempotent early-return checks above,
|
|
989
|
-
# so a no-op "already running" start never mints an empty run log dir). The
|
|
990
|
-
# launcher's `tee -a` needs the file's parent to exist when the pane runs.
|
|
991
|
-
mkdir -p "$(dirname "$startup_capture")" 2>/dev/null || true
|
|
992
|
-
: >"$startup_capture" 2>/dev/null || true
|
|
993
|
-
|
|
994
|
-
if ! "$tmux_bin" -L "$socket" -f "$conf" new-session -d -s "$session" \
|
|
995
|
-
-x "${COLUMNS:-200}" -y "${LINES:-50}" "$inner_cmd"; then
|
|
996
|
-
echo "❌ aiur failed to start; captured output:" >&2
|
|
997
|
-
tail -n 30 "$startup_capture" 2>/dev/null | sed 's/^/ /' >&2 || true
|
|
998
|
-
rm -f "${launch_tempfiles[@]}" 2>/dev/null || true
|
|
999
|
-
release_aiur_launch_lock "$launch_lock"
|
|
1000
|
-
exit 1
|
|
1001
|
-
fi
|
|
1002
|
-
|
|
1003
|
-
# Each instance has its own tmux socket. Publish the executable shipped next
|
|
1004
|
-
# to this engine before the TUI opens a chat pane; tmux receives the path as
|
|
1005
|
-
# one argument, including when the npm install directory contains spaces.
|
|
1006
|
-
if [ "$mode" = "foreground" ]; then
|
|
1007
|
-
local ctrlc_helper="$engine_dir/aiur-pane-ctrlc"
|
|
1008
|
-
if [ ! -x "$ctrlc_helper" ] ||
|
|
1009
|
-
! "$tmux_bin" -L "$socket" -f "$conf" set-option -g @aiur_ctrlc "$ctrlc_helper"; then
|
|
1010
|
-
"$tmux_bin" -L "$socket" -f "$conf" kill-session -t "$session" 2>/dev/null || true
|
|
1011
|
-
rm -f "${launch_tempfiles[@]}" 2>/dev/null || true
|
|
1012
|
-
release_aiur_launch_lock "$launch_lock"
|
|
1013
|
-
die "aiur pane control helper is unavailable at $ctrlc_helper"
|
|
1014
|
-
fi
|
|
1015
|
-
fi
|
|
1016
|
-
|
|
1017
|
-
# Title the agent-list pane (the only pane in the fresh session). The conf's
|
|
1018
|
-
# `pane-border-status`/`pane-border-format` render it; PaneManager titles the
|
|
1019
|
-
# chat panes it opens. Best-effort — a missing title just shows the default.
|
|
1020
|
-
"$tmux_bin" -L "$socket" -f "$conf" select-pane -t "$session" -T "AIUR Agents" 2>/dev/null || true
|
|
1021
|
-
|
|
1022
|
-
# Grace window: surface a boot crash instead of attaching to nothing. Prove
|
|
1023
|
-
# the control plane can answer before declaring startup usable: background
|
|
1024
|
-
# control commands depend on that RPC path, and foreground automation must not
|
|
1025
|
-
# treat a pre-TUI application crash as a successful `tmux attach`.
|
|
1026
|
-
local require_control=1
|
|
1027
|
-
if ! wait_for_session_startup "$tmux_bin" "$socket" "$conf" "$session" "$startup_capture" "$require_control"; then
|
|
1028
|
-
print_config_status "$startup_capture"
|
|
1029
|
-
# The application can write the ledger handoff and then crash before its
|
|
1030
|
-
# control plane becomes ready. Inspect the completed dump before launcher
|
|
1031
|
-
# cleanup removes that handoff; this path intentionally writes no generic
|
|
1032
|
-
# crash marker when there is no new completed dump.
|
|
1033
|
-
record_new_crash_dump_alert "$AIUR_ALERT_LEDGER_PATH_FILE" "$crash_dump_baseline_file"
|
|
1034
|
-
if [ "$mode" = "background" ]; then
|
|
1035
|
-
"$tmux_bin" -L "$socket" -f "$conf" kill-session -t "$session" 2>/dev/null || true
|
|
1036
|
-
reap_aiur_agents "$socket" "$AIUR_AGENT_TMPFILE"
|
|
1037
|
-
kill_beams_matching "-name ${AIUR_RELEASE_NODE}"
|
|
1038
|
-
fi
|
|
1039
|
-
release_aiur_launch_lock "$launch_lock"
|
|
1040
|
-
_session_launch_lock=""
|
|
1041
|
-
[ "$mode" = "foreground" ] && return 1
|
|
1042
|
-
rm -f "${launch_tempfiles[@]}" 2>/dev/null || true
|
|
1043
|
-
exit 1
|
|
1044
|
-
fi
|
|
1045
|
-
|
|
1046
|
-
write_aiur_instance_record "$session" "$socket"
|
|
1047
|
-
release_aiur_launch_lock "$launch_lock"
|
|
1048
|
-
_session_launch_lock=""
|
|
1049
|
-
print_config_status "$startup_capture"
|
|
1050
|
-
print_dashboard_status "$no_dashboard" "$startup_capture"
|
|
1051
|
-
# The `aiur run` version notice: a cheap local state read (the daemon's
|
|
1052
|
-
# `Aiur.Upgrade` check writes it during boot), never a network call, so it
|
|
1053
|
-
# can never delay startup. Silent under a dev launcher and when opted out.
|
|
1054
|
-
# `|| true` keeps a display failure from ever failing the run — the notice
|
|
1055
|
-
# is strictly additive.
|
|
1056
|
-
maybe_surface_upgrade_notice || true
|
|
1057
|
-
|
|
1058
|
-
if [ "$mode" = "foreground" ]; then
|
|
1059
|
-
echo "aiur foreground tmux socket ${socket}, session ${session}" >&2
|
|
1060
|
-
fi
|
|
1061
|
-
|
|
1062
|
-
if [ "$mode" = "background" ]; then
|
|
1063
|
-
# Fresh run: drop any stale crash/stop state from a prior dead instance so
|
|
1064
|
-
# `status` doesn't report a phantom orphan and the watchdog starts clean.
|
|
1065
|
-
rm -f "$(aiur_stop_sentinel_path)" "$(aiur_crash_marker_path)" 2>/dev/null || true
|
|
1066
|
-
|
|
1067
|
-
local background_watchdog_pid
|
|
1068
|
-
background_watchdog_pid="$(start_beam_death_watchdog \
|
|
1069
|
-
"-name ${AIUR_RELEASE_NODE}" "$socket" "$AIUR_AGENT_TMPFILE" 1 1 \
|
|
1070
|
-
"$AIUR_RELEASE_NODE" "${AIUR_LOGS_ROOT:-}" \
|
|
1071
|
-
"$(aiur_stop_sentinel_path)" "$(aiur_crash_marker_path)" "$AIUR_WORKSPACE_ROOT_FILE" \
|
|
1072
|
-
"$AIUR_ALERT_LEDGER_PATH_FILE" "$crash_dump_baseline_file")"
|
|
1073
|
-
disown "$background_watchdog_pid" 2>/dev/null || true
|
|
1074
|
-
echo "aiur started in the background (tmux socket ${socket}, session ${session}). Attach with: aiur" >&2
|
|
1075
|
-
# Keep $startup_capture (boot.out.log) for the run's lifetime; only the
|
|
1076
|
-
# transient argv file is no longer needed.
|
|
1077
|
-
rm -f "$argv_file" 2>/dev/null || true
|
|
1078
|
-
return 0
|
|
1079
|
-
fi
|
|
1080
|
-
|
|
1081
|
-
# Arm the BEAM-death watchdog before attaching. If the BEAM crashes
|
|
1082
|
-
# (:emfile) mid-run, agent windows keep the orphaned session alive so the
|
|
1083
|
-
# `tmux attach` below never returns and the EXIT trap never fires — the
|
|
1084
|
-
# watchdog is the external reaper that survives the dead BEAM, kill-servers
|
|
1085
|
-
# the session, and unblocks the attach. It polls the node name rather than a
|
|
1086
|
-
# captured pid, so another Aiur instance from the same release cannot hold it open.
|
|
1087
|
-
_session_watchdog_pid="$(start_beam_death_watchdog \
|
|
1088
|
-
"-name ${AIUR_RELEASE_NODE}" "$socket" "$AIUR_AGENT_TMPFILE" 1 0 \
|
|
1089
|
-
"$AIUR_RELEASE_NODE" "" "" "" "$AIUR_WORKSPACE_ROOT_FILE" \
|
|
1090
|
-
"$AIUR_ALERT_LEDGER_PATH_FILE" "$crash_dump_baseline_file")"
|
|
1091
|
-
|
|
1092
|
-
# Foreground: attach the UI. Do not exec — that would drop the teardown trap.
|
|
1093
|
-
# Avoid process substitution here: some sandboxed non-TTY launchers reject
|
|
1094
|
-
# opening /dev/fd/* during the real manual-test wrapper path.
|
|
1095
|
-
attach_tmux_session "$tmux_bin" "$socket" "$conf" "$session"
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
attach_tmux_session() {
|
|
1099
|
-
local tmux_bin="$1" socket="$2" conf="$3" session="$4"
|
|
1100
|
-
local attach_stderr attach_code
|
|
1101
|
-
attach_stderr="$(mktemp "${TMPDIR:-/tmp}/aiur-attach-stderr.XXXXXX")"
|
|
1102
|
-
if "$tmux_bin" -L "$socket" -f "$conf" attach -t "$session" 2>"$attach_stderr"; then
|
|
1103
|
-
attach_code=0
|
|
1104
|
-
else
|
|
1105
|
-
attach_code=$?
|
|
1106
|
-
fi
|
|
1107
|
-
grep -v -F "[server exited]" "$attach_stderr" >&2 || true
|
|
1108
|
-
rm -f "$attach_stderr" 2>/dev/null || true
|
|
1109
|
-
return "$attach_code"
|
|
1110
|
-
}
|
|
1111
|
-
|
|
1112
|
-
resolve_tmux_conf() {
|
|
1113
|
-
if [ -n "${AIUR_TMUX_CONF:-}" ] && [ -f "${AIUR_TMUX_CONF:-}" ]; then
|
|
1114
|
-
printf '%s\n' "$AIUR_TMUX_CONF"
|
|
1115
|
-
return
|
|
1116
|
-
fi
|
|
1117
|
-
local user_conf="${XDG_CONFIG_HOME:-$HOME/.config}/aiur/tmux.conf"
|
|
1118
|
-
if [ -f "$user_conf" ]; then
|
|
1119
|
-
printf '%s\n' "$user_conf"
|
|
1120
|
-
return
|
|
1121
|
-
fi
|
|
1122
|
-
printf '%s\n' "$engine_dir/../share/aiur.tmux.conf"
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
# TERM-then-KILL every BEAM whose command line matches $1. Reaps a stale node by
|
|
1126
|
-
# name (covers orphans from any release dir): under the unified identity a dev
|
|
1127
|
-
# `_build` BEAM and an installed BEAM both claim the same node name, so a
|
|
1128
|
-
# release-path pgrep misses whichever one this run didn't launch.
|
|
1129
|
-
# TERM the node(s) matching $pattern, give them $grace_ticks (0.1s) to exit
|
|
1130
|
-
# gracefully, then SIGKILL whatever remains. The default grace is short (3s)
|
|
1131
|
-
# because startup reclaim wants to clear a stale BEAM quickly; the stop paths
|
|
1132
|
-
# pass a longer grace so the BEAM can finish its own agent reap + session
|
|
1133
|
-
# deletion instead of being SIGKILLed mid-cleanup (which is what orphaned agent
|
|
1134
|
-
# processes on `aiur stop`). Callers may override the grace internally; invalid
|
|
1135
|
-
# or excessive values fall back to the 3s startup-reclaim default instead of
|
|
1136
|
-
# skipping the wait and jumping straight to SIGKILL.
|
|
1137
|
-
kill_beams_matching() {
|
|
1138
|
-
local pattern="$1" grace_ticks="${2:-30}" pids pid waited=0
|
|
1139
|
-
case "$grace_ticks" in
|
|
1140
|
-
"" | *[!0-9]*) grace_ticks=30 ;;
|
|
1141
|
-
*)
|
|
1142
|
-
# Reject overlong digit strings before numeric comparison so shell integer
|
|
1143
|
-
# overflow cannot turn an excessive grace into an immediate SIGKILL.
|
|
1144
|
-
if [ "${#grace_ticks}" -gt 5 ] ||
|
|
1145
|
-
{ [ "${#grace_ticks}" -eq 5 ] && [ "$grace_ticks" -gt 36000 ]; }; then
|
|
1146
|
-
grace_ticks=30
|
|
1147
|
-
fi
|
|
1148
|
-
;;
|
|
1149
|
-
esac
|
|
1150
|
-
pids="$(pgrep -f -- "$pattern" 2>/dev/null || true)"
|
|
1151
|
-
[ -n "$pids" ] || return 0
|
|
1152
|
-
for pid in $pids; do kill -TERM "$pid" 2>/dev/null || true; done
|
|
1153
|
-
while [ "$waited" -lt "$grace_ticks" ]; do
|
|
1154
|
-
pids="$(pgrep -f -- "$pattern" 2>/dev/null || true)"
|
|
1155
|
-
[ -z "$pids" ] && break
|
|
1156
|
-
sleep 0.1
|
|
1157
|
-
waited=$((waited + 1))
|
|
1158
|
-
done
|
|
1159
|
-
pids="$(pgrep -f -- "$pattern" 2>/dev/null || true)"
|
|
1160
|
-
for pid in $pids; do kill -KILL "$pid" 2>/dev/null || true; done
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
pid_owner() {
|
|
1164
|
-
ps -p "$1" -o user= 2>/dev/null | awk '{print $1}' || true
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
|
-
pid_command() {
|
|
1168
|
-
ps -p "$1" -o command= 2>/dev/null || true
|
|
1169
|
-
}
|
|
1170
|
-
|
|
1171
|
-
pid_ppid() {
|
|
1172
|
-
ps -p "$1" -o ppid= 2>/dev/null | awk '{print $1}' || true
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
|
-
pid_cwd() {
|
|
1176
|
-
local pid="$1" cwd
|
|
1177
|
-
if [ -e "/proc/$pid/cwd" ]; then
|
|
1178
|
-
readlink "/proc/$pid/cwd" 2>/dev/null || true
|
|
1179
|
-
return 0
|
|
1180
|
-
fi
|
|
1181
|
-
if command -v lsof >/dev/null 2>&1; then
|
|
1182
|
-
cwd="$(lsof -a -p "$pid" -d cwd -Fn 2>/dev/null | sed -n 's/^n//p' | head -n 1 || true)"
|
|
1183
|
-
[ -n "$cwd" ] && printf '%s\n' "$cwd"
|
|
1184
|
-
fi
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
kill_pid_with_escalation() {
|
|
1188
|
-
local pid="$1" waited=0
|
|
1189
|
-
[ -n "$pid" ] || return 0
|
|
1190
|
-
kill -TERM "$pid" 2>/dev/null || true
|
|
1191
|
-
while [ "$waited" -lt 20 ]; do
|
|
1192
|
-
kill -0 "$pid" 2>/dev/null || return 0
|
|
1193
|
-
sleep 0.1
|
|
1194
|
-
waited=$((waited + 1))
|
|
1195
|
-
done
|
|
1196
|
-
kill -KILL "$pid" 2>/dev/null || true
|
|
1197
|
-
}
|
|
1198
|
-
|
|
1199
|
-
aiur_node_from_command() {
|
|
1200
|
-
local cmd="$1" node
|
|
1201
|
-
node="$(printf '%s\n' "$cmd" | sed -n "s/.*--name[[:space:]]\\(aiur-${USER}[-A-Za-z0-9_]*@127\\.0\\.0\\.1\\).*/\\1/p" | head -n 1)"
|
|
1202
|
-
[ -n "$node" ] && printf '%s\n' "$node"
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
aiur_release_root_from_command() {
|
|
1206
|
-
local cmd="$1"
|
|
1207
|
-
if [[ "$cmd" =~ --boot-var[[:space:]]+RELEASE_LIB[[:space:]]+([^[:space:]]+)/lib ]]; then
|
|
1208
|
-
printf '%s\n' "${BASH_REMATCH[1]}"
|
|
1209
|
-
return 0
|
|
1210
|
-
fi
|
|
1211
|
-
if [[ "$cmd" =~ ([^[:space:]]*/src/_build/dev/rel/aiur)/releases/ ]]; then
|
|
1212
|
-
printf '%s\n' "${BASH_REMATCH[1]}"
|
|
1213
|
-
return 0
|
|
1214
|
-
fi
|
|
1215
|
-
if [[ "$cmd" =~ ([^[:space:]]*/release)/releases/ ]]; then
|
|
1216
|
-
printf '%s\n' "${BASH_REMATCH[1]}"
|
|
1217
|
-
return 0
|
|
1218
|
-
fi
|
|
1219
|
-
return 1
|
|
1220
|
-
}
|
|
1221
|
-
|
|
1222
|
-
workspace_root_from_aiur_release_root() {
|
|
1223
|
-
local release_root="${1%/}"
|
|
1224
|
-
case "$release_root" in
|
|
1225
|
-
*/src/_build/dev/rel/aiur)
|
|
1226
|
-
printf '%s\n' "${release_root%/src/_build/dev/rel/aiur}"
|
|
1227
|
-
;;
|
|
1228
|
-
*)
|
|
1229
|
-
printf '%s\n' "$release_root"
|
|
1230
|
-
;;
|
|
1231
|
-
esac
|
|
1232
|
-
}
|
|
1233
|
-
|
|
1234
|
-
aiur_node_tmux_session_alive() {
|
|
1235
|
-
local node="$1" short tmux_bin
|
|
1236
|
-
short="${node%@*}"
|
|
1237
|
-
tmux_bin="$(command -v tmux || true)"
|
|
1238
|
-
[ -n "$tmux_bin" ] || return 1
|
|
1239
|
-
"$tmux_bin" -L "$short" has-session -t "${short}-default" 2>/dev/null
|
|
1240
|
-
}
|
|
1241
|
-
|
|
1242
|
-
stale_manual_smoke_beam_inventory() {
|
|
1243
|
-
local pid cmd node release_root workspace_root owner
|
|
1244
|
-
aiur_resolve_identity
|
|
1245
|
-
|
|
1246
|
-
for pid in $(pgrep -f -- "beam\\.smp.*--name aiur-${USER}.*@127\\.0\\.0\\.1" 2>/dev/null || true); do
|
|
1247
|
-
[ -n "$pid" ] || continue
|
|
1248
|
-
owner="$(pid_owner "$pid")"
|
|
1249
|
-
[ "$owner" = "$USER" ] || continue
|
|
1250
|
-
|
|
1251
|
-
cmd="$(pid_command "$pid")"
|
|
1252
|
-
node="$(aiur_node_from_command "$cmd")"
|
|
1253
|
-
[ -n "$node" ] || continue
|
|
1254
|
-
|
|
1255
|
-
release_root="$(aiur_release_root_from_command "$cmd" || true)"
|
|
1256
|
-
[ -n "$release_root" ] || continue
|
|
1257
|
-
workspace_root="$(workspace_root_from_aiur_release_root "$release_root")"
|
|
1258
|
-
|
|
1259
|
-
# Scope this broad cleanup to stale issue/manual-smoke workspaces. The
|
|
1260
|
-
# current instance cleanup path handles the active daemon's own node.
|
|
1261
|
-
case "$workspace_root" in
|
|
1262
|
-
*/aiur-workspaces/*) ;;
|
|
1263
|
-
*) continue ;;
|
|
1264
|
-
esac
|
|
1265
|
-
|
|
1266
|
-
# A matching live aiur tmux session means this node may still be in use.
|
|
1267
|
-
aiur_node_tmux_session_alive "$node" && continue
|
|
1268
|
-
|
|
1269
|
-
printf '%s\t%s\t%s\t%s\n' "$pid" "$node" "$workspace_root" "$release_root"
|
|
1270
|
-
done
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
manual_smoke_wrapper_tmux_sockets() {
|
|
1274
|
-
local tmux_bin sockdir path name panes current pane_path start manual_context all_sleep
|
|
1275
|
-
tmux_bin="$(command -v tmux || true)"
|
|
1276
|
-
[ -n "$tmux_bin" ] || return 0
|
|
1277
|
-
sockdir="${TMUX_TMPDIR:-/tmp}/tmux-$(id -u)"
|
|
1278
|
-
[ -d "$sockdir" ] || return 0
|
|
1279
|
-
|
|
1280
|
-
for path in "$sockdir"/*; do
|
|
1281
|
-
[ -S "$path" ] || continue
|
|
1282
|
-
name="${path##*/}"
|
|
1283
|
-
case "$name" in
|
|
1284
|
-
codex-driver-* | codex-aiur* | codex-[0-9]* | claude-driver*) ;;
|
|
1285
|
-
*) continue ;;
|
|
1286
|
-
esac
|
|
1287
|
-
panes="$("$tmux_bin" -L "$name" list-panes -a -F '#{pane_current_command} #{pane_current_path} #{pane_start_command}' 2>/dev/null || true)"
|
|
1288
|
-
[ -n "$panes" ] || continue
|
|
1289
|
-
manual_context=0
|
|
1290
|
-
all_sleep=1
|
|
1291
|
-
while IFS=$'\t' read -r current pane_path start; do
|
|
1292
|
-
case "$start" in
|
|
1293
|
-
*aiurdev\ --test* | *scripts/aiurdev\ --test*) manual_context=1 ;;
|
|
1294
|
-
esac
|
|
1295
|
-
[ "$current" = "sleep" ] || all_sleep=0
|
|
1296
|
-
done <<<"$panes"
|
|
1297
|
-
[ "$manual_context" -eq 1 ] && [ "$all_sleep" -eq 1 ] && printf '%s\n' "$name"
|
|
1298
|
-
done
|
|
1299
|
-
}
|
|
1300
|
-
|
|
1301
|
-
orphaned_opencode_attach_inventory() {
|
|
1302
|
-
local pid owner ppid cmd cwd
|
|
1303
|
-
for pid in $(pgrep -f -- "opencode attach http://127\\.0\\.0\\.1:" 2>/dev/null || true); do
|
|
1304
|
-
owner="$(pid_owner "$pid")"
|
|
1305
|
-
[ "$owner" = "$USER" ] || continue
|
|
1306
|
-
ppid="$(pid_ppid "$pid")"
|
|
1307
|
-
[ "$ppid" = "1" ] || continue
|
|
1308
|
-
cwd="$(pid_cwd "$pid")"
|
|
1309
|
-
case "$cwd" in
|
|
1310
|
-
*/aiur-workspaces/*) ;;
|
|
1311
|
-
*) continue ;;
|
|
1312
|
-
esac
|
|
1313
|
-
cmd="$(pid_command "$pid")"
|
|
1314
|
-
case "$cmd" in
|
|
1315
|
-
*"opencode attach http://127.0.0.1:"*) printf '%s\t%s\t%s\n' "$pid" "$cwd" "$cmd" ;;
|
|
1316
|
-
esac
|
|
1317
|
-
done
|
|
1318
|
-
}
|
|
1319
|
-
|
|
1320
|
-
report_stale_manual_smoke() {
|
|
1321
|
-
local found=0 pid node workspace_root release_root socket cwd cmd
|
|
1322
|
-
|
|
1323
|
-
while IFS=$'\t' read -r pid node workspace_root release_root; do
|
|
1324
|
-
[ -n "$pid" ] || continue
|
|
1325
|
-
if [ "$found" -eq 0 ]; then
|
|
1326
|
-
echo "aiur: stale manual-smoke leftovers detected:" >&2
|
|
1327
|
-
found=1
|
|
1328
|
-
fi
|
|
1329
|
-
echo " BEAM pid=${pid} node=${node} workspace=${workspace_root} release=${release_root}" >&2
|
|
1330
|
-
done < <(stale_manual_smoke_beam_inventory)
|
|
1331
|
-
|
|
1332
|
-
while IFS= read -r socket; do
|
|
1333
|
-
[ -n "$socket" ] || continue
|
|
1334
|
-
if [ "$found" -eq 0 ]; then
|
|
1335
|
-
echo "aiur: stale manual-smoke leftovers detected:" >&2
|
|
1336
|
-
found=1
|
|
1337
|
-
fi
|
|
1338
|
-
echo " wrapper tmux socket=${socket}" >&2
|
|
1339
|
-
done < <(manual_smoke_wrapper_tmux_sockets)
|
|
1340
|
-
|
|
1341
|
-
while IFS=$'\t' read -r pid cwd cmd; do
|
|
1342
|
-
[ -n "$pid" ] || continue
|
|
1343
|
-
if [ "$found" -eq 0 ]; then
|
|
1344
|
-
echo "aiur: stale manual-smoke leftovers detected:" >&2
|
|
1345
|
-
found=1
|
|
1346
|
-
fi
|
|
1347
|
-
echo " orphan opencode attach pid=${pid} workspace=${cwd}" >&2
|
|
1348
|
-
done < <(orphaned_opencode_attach_inventory)
|
|
1349
|
-
|
|
1350
|
-
if [ "$found" -eq 1 ]; then
|
|
1351
|
-
echo "aiur: run 'aiur cleanup-stale' to TERM/KILL only these same-user Aiur leftovers." >&2
|
|
1352
|
-
fi
|
|
1353
|
-
|
|
1354
|
-
return "$found"
|
|
1355
|
-
}
|
|
1356
|
-
|
|
1357
|
-
preflight_stale_manual_smoke() {
|
|
1358
|
-
report_stale_manual_smoke >/dev/null || true
|
|
1359
|
-
}
|
|
1360
|
-
|
|
1361
|
-
reap_stale_manual_smoke() {
|
|
1362
|
-
local include_wrappers="${1:-0}"
|
|
1363
|
-
local pid node workspace_root release_root socket cwd cmd tmux_bin reaped=0
|
|
1364
|
-
|
|
1365
|
-
while IFS=$'\t' read -r pid node workspace_root release_root; do
|
|
1366
|
-
[ -n "$pid" ] || continue
|
|
1367
|
-
echo "aiur: reaping stale BEAM pid=${pid} node=${node} workspace=${workspace_root}" >&2
|
|
1368
|
-
kill_pid_with_escalation "$pid"
|
|
1369
|
-
reaped=$((reaped + 1))
|
|
1370
|
-
done < <(stale_manual_smoke_beam_inventory)
|
|
1371
|
-
|
|
1372
|
-
tmux_bin="$(command -v tmux || true)"
|
|
1373
|
-
if [ "$include_wrappers" = "1" ] && [ -n "$tmux_bin" ]; then
|
|
1374
|
-
while IFS= read -r socket; do
|
|
1375
|
-
[ -n "$socket" ] || continue
|
|
1376
|
-
echo "aiur: reaping stale wrapper tmux socket=${socket}" >&2
|
|
1377
|
-
"$tmux_bin" -L "$socket" kill-server 2>/dev/null || true
|
|
1378
|
-
reaped=$((reaped + 1))
|
|
1379
|
-
done < <(manual_smoke_wrapper_tmux_sockets)
|
|
1380
|
-
fi
|
|
1381
|
-
|
|
1382
|
-
while IFS=$'\t' read -r pid cwd cmd; do
|
|
1383
|
-
[ -n "$pid" ] || continue
|
|
1384
|
-
echo "aiur: reaping orphan opencode attach pid=${pid}" >&2
|
|
1385
|
-
kill_pid_with_escalation "$pid"
|
|
1386
|
-
reaped=$((reaped + 1))
|
|
1387
|
-
done < <(orphaned_opencode_attach_inventory)
|
|
1388
|
-
|
|
1389
|
-
if [ "$reaped" -eq 0 ]; then
|
|
1390
|
-
echo "aiur: no stale manual-smoke leftovers found" >&2
|
|
1391
|
-
fi
|
|
1392
|
-
}
|
|
1393
|
-
|
|
1394
|
-
# Echo $1 and every descendant pid, depth-first. Mac-safe (`pgrep -P`, no
|
|
1395
|
-
# /proc), mirroring Aiur.RemoteControl.collect_descendants so the launcher reaps
|
|
1396
|
-
# the same tree the BEAM-side reaper would. The recorded agent pid is a bash
|
|
1397
|
-
# `-c` wrapper; the model process (claude --print / codex) is its child and
|
|
1398
|
-
# reparents to init if only the wrapper is signalled — orphan-and-survive is
|
|
1399
|
-
# exactly the bug — so the whole tree must be collected before any kill lands.
|
|
1400
|
-
agent_pid_tree() {
|
|
1401
|
-
local root="$1" child
|
|
1402
|
-
printf '%s\n' "$root"
|
|
1403
|
-
for child in $(pgrep -P "$root" 2>/dev/null || true); do
|
|
1404
|
-
agent_pid_tree "$child"
|
|
1405
|
-
done
|
|
1406
|
-
}
|
|
1407
|
-
|
|
1408
|
-
# pid-reuse guard: succeeds when pid $1 is alive AND its command still contains
|
|
1409
|
-
# the comm substring $2 the BEAM recorded. An empty comm kills unconditionally.
|
|
1410
|
-
# Mirrors the BEAM-side cmdline guard (Aiur.ProcessReaper) but Mac-safe via
|
|
1411
|
-
# `ps -o command=`, so a recycled pid whose command no longer matches is spared.
|
|
1412
|
-
agent_pid_matches() {
|
|
1413
|
-
local pid="$1" comm="$2" cmd
|
|
1414
|
-
kill -0 "$pid" 2>/dev/null || return 1
|
|
1415
|
-
[ -n "$comm" ] || return 0
|
|
1416
|
-
cmd="$(ps -p "$pid" -o command= 2>/dev/null || true)"
|
|
1417
|
-
case "$cmd" in *"$comm"*) return 0 ;; *) return 1 ;; esac
|
|
1418
|
-
}
|
|
1419
|
-
|
|
1420
|
-
# Reap every aiur agent the BEAM can no longer reap itself.
|
|
1421
|
-
#
|
|
1422
|
-
# $1 socket aiur tmux socket (-L); kill-server nukes all panes (may be empty)
|
|
1423
|
-
# $2 pidfile AIUR_AGENT_TMPFILE (BEAM-written agent refs; may be empty/missing)
|
|
1424
|
-
#
|
|
1425
|
-
# tmux runs on aiur's PRIVATE socket (-L aiur-$USER), so kill-server tears down
|
|
1426
|
-
# every REPL/chat pane agent in one shot AND leaves no live aiur tmux server —
|
|
1427
|
-
# never touching the Executor’s own default tmux. Headless agents (claude/codex
|
|
1428
|
-
# app-servers spawned via Port) are bare OS processes that reparent to init on a
|
|
1429
|
-
# BEAM crash; kill-server can't see them, so they're reaped from the pidfile by
|
|
1430
|
-
# process tree, checking the recorded command. A recycled pid with a matching
|
|
1431
|
-
# command is still possible (#2844). Idempotent.
|
|
1432
|
-
reap_aiur_agents() {
|
|
1433
|
-
local socket="$1" pidfile="${2:-}"
|
|
1434
|
-
local tmux_bin
|
|
1435
|
-
tmux_bin="$(command -v tmux || true)"
|
|
1436
|
-
|
|
1437
|
-
if [ -n "$tmux_bin" ] && [ -n "$socket" ]; then
|
|
1438
|
-
"$tmux_bin" -L "$socket" kill-server 2>/dev/null || true
|
|
1439
|
-
fi
|
|
1440
|
-
|
|
1441
|
-
[ -n "$pidfile" ] && [ -r "$pidfile" ] || return 0
|
|
1442
|
-
|
|
1443
|
-
# Snapshot the full process tree of every still-matching headless agent before
|
|
1444
|
-
# signalling, so descendants that reparent mid-reap are already on the list.
|
|
1445
|
-
local kind pid comm tree=() p
|
|
1446
|
-
while read -r kind pid comm; do
|
|
1447
|
-
[ "$kind" = "pid" ] && [ -n "$pid" ] || continue
|
|
1448
|
-
agent_pid_matches "$pid" "$comm" || continue
|
|
1449
|
-
while IFS= read -r p; do tree+=("$p"); done < <(agent_pid_tree "$pid")
|
|
1450
|
-
done <"$pidfile"
|
|
1451
|
-
|
|
1452
|
-
[ "${#tree[@]}" -gt 0 ] || return 0
|
|
1453
|
-
|
|
1454
|
-
for p in "${tree[@]}"; do kill -TERM "$p" 2>/dev/null || true; done
|
|
1455
|
-
local waited=0
|
|
1456
|
-
while [ "$waited" -lt 20 ]; do
|
|
1457
|
-
local any=0
|
|
1458
|
-
for p in "${tree[@]}"; do kill -0 "$p" 2>/dev/null && { any=1; break; }; done
|
|
1459
|
-
[ "$any" -eq 0 ] && break
|
|
1460
|
-
sleep 0.1
|
|
1461
|
-
waited=$((waited + 1))
|
|
1462
|
-
done
|
|
1463
|
-
for p in "${tree[@]}"; do kill -KILL "$p" 2>/dev/null || true; done
|
|
1464
|
-
}
|
|
1465
|
-
|
|
1466
|
-
# Stable, per-instance state paths the background BEAM-death machinery shares
|
|
1467
|
-
# across `run`, `stop`, and `status`. Keyed by the (instance-keyed) release node
|
|
1468
|
-
# rather than the tmux socket so `status` — which resolves the node but not the
|
|
1469
|
-
# socket — agrees on the same files. AIUR_BG_STATE_DIR and AIUR_RELEASE_NODE are
|
|
1470
|
-
# resolved by aiur_resolve_identity, which every one of those paths calls first.
|
|
1471
|
-
aiur_state_slug() {
|
|
1472
|
-
printf '%s' "${AIUR_RELEASE_NODE:-aiur}" | tr -c 'A-Za-z0-9._-' '_'
|
|
1473
|
-
}
|
|
1474
|
-
|
|
1475
|
-
aiur_instances_dir() {
|
|
1476
|
-
printf '%s/instances' "${AIUR_BG_STATE_DIR:?}"
|
|
1477
|
-
}
|
|
1478
|
-
|
|
1479
|
-
aiur_instance_record_path() {
|
|
1480
|
-
printf '%s/%s.instance' "$(aiur_instances_dir)" "$(aiur_state_slug)"
|
|
1481
|
-
}
|
|
1482
|
-
|
|
1483
|
-
aiur_launch_lock_path() {
|
|
1484
|
-
printf '%s/locks/%s.launch' "${AIUR_BG_STATE_DIR:?}" "$(aiur_state_slug)"
|
|
1485
|
-
}
|
|
1486
|
-
|
|
1487
|
-
acquire_aiur_launch_lock() {
|
|
1488
|
-
local lock="$1" owner_file="$1/owner" recovery_lock="$1.recovery"
|
|
1489
|
-
local owner tick lock_age
|
|
1490
|
-
local max_ticks="${AIUR_LAUNCH_LOCK_TICKS:-${AIUR_NODE_GRACE_TICKS:-1200}}"
|
|
1491
|
-
local ownerless_stale_seconds="${AIUR_OWNERLESS_LOCK_STALE_SECONDS:-120}"
|
|
1492
|
-
mkdir -p "$(dirname "$lock")" 2>/dev/null || return 1
|
|
1493
|
-
|
|
1494
|
-
for ((tick = 0; tick < max_ticks; tick++)); do
|
|
1495
|
-
if mkdir "$lock" 2>/dev/null; then
|
|
1496
|
-
printf '%s\n' "$$" >"$owner_file" || { rmdir "$lock" 2>/dev/null || true; return 1; }
|
|
1497
|
-
return 0
|
|
1498
|
-
fi
|
|
1499
|
-
|
|
1500
|
-
owner="$(sed -n '1p' "$owner_file" 2>/dev/null || true)"
|
|
1501
|
-
if [[ "$owner" =~ ^[0-9]+$ ]] && kill -0 "$owner" 2>/dev/null; then
|
|
1502
|
-
sleep 0.1
|
|
1503
|
-
continue
|
|
1504
|
-
fi
|
|
1505
|
-
|
|
1506
|
-
lock_age="$(aiur_lock_age_seconds "$lock")"
|
|
1507
|
-
if [ -z "$owner" ] && { [ -z "$lock_age" ] || [ "$lock_age" -lt "$ownerless_stale_seconds" ]; }; then
|
|
1508
|
-
sleep 0.1
|
|
1509
|
-
continue
|
|
1510
|
-
fi
|
|
1511
|
-
|
|
1512
|
-
# Only one contender may reclaim a dead lock. Re-read ownership while the
|
|
1513
|
-
# recovery mutex is held so another contender cannot delete a newly claimed
|
|
1514
|
-
# launch directory after losing the original stale-lock race.
|
|
1515
|
-
if mkdir "$recovery_lock" 2>/dev/null; then
|
|
1516
|
-
owner="$(sed -n '1p' "$owner_file" 2>/dev/null || true)"
|
|
1517
|
-
lock_age="$(aiur_lock_age_seconds "$lock")"
|
|
1518
|
-
if { [ -z "$owner" ] && [ -n "$lock_age" ] && [ "$lock_age" -ge "$ownerless_stale_seconds" ]; } || \
|
|
1519
|
-
{ [[ "$owner" =~ ^[0-9]+$ ]] && ! kill -0 "$owner" 2>/dev/null; }; then
|
|
1520
|
-
rm -f "$owner_file" 2>/dev/null || true
|
|
1521
|
-
rmdir "$lock" 2>/dev/null || true
|
|
1522
|
-
fi
|
|
1523
|
-
rmdir "$recovery_lock" 2>/dev/null || true
|
|
1524
|
-
fi
|
|
1525
|
-
sleep 0.1
|
|
1526
|
-
done
|
|
1527
|
-
|
|
1528
|
-
echo "aiur: another launch for this directory is still in progress; retry aiur" >&2
|
|
1529
|
-
return 1
|
|
1530
|
-
}
|
|
1531
|
-
|
|
1532
|
-
aiur_lock_age_seconds() {
|
|
1533
|
-
local path="$1" now modified
|
|
1534
|
-
now="$(date +%s 2>/dev/null || true)"
|
|
1535
|
-
modified="$(stat -c %Y "$path" 2>/dev/null || stat -f %m "$path" 2>/dev/null || true)"
|
|
1536
|
-
[[ "$now" =~ ^[0-9]+$ ]] && [[ "$modified" =~ ^[0-9]+$ ]] || return 0
|
|
1537
|
-
[ "$now" -ge "$modified" ] || return 0
|
|
1538
|
-
printf '%s' "$((now - modified))"
|
|
1539
|
-
}
|
|
1540
|
-
|
|
1541
|
-
release_aiur_launch_lock() {
|
|
1542
|
-
local lock="${1:-}" owner_file owner
|
|
1543
|
-
[ -n "$lock" ] || return 0
|
|
1544
|
-
owner_file="$lock/owner"
|
|
1545
|
-
owner="$(sed -n '1p' "$owner_file" 2>/dev/null || true)"
|
|
1546
|
-
[ "$owner" = "$$" ] || return 0
|
|
1547
|
-
rm -f "$owner_file" 2>/dev/null || true
|
|
1548
|
-
rmdir "$lock" 2>/dev/null || true
|
|
1549
|
-
}
|
|
1550
|
-
|
|
1551
|
-
write_aiur_instance_record() {
|
|
1552
|
-
local session="$1" socket="$2" write_mode="${3:-replace}" record_dir record tmp root
|
|
1553
|
-
record_dir="$(aiur_instances_dir)"
|
|
1554
|
-
record="$(aiur_instance_record_path)"
|
|
1555
|
-
root="$(canonical_workspace_root "${AIUR_PROJECT_ROOT:-}")"
|
|
1556
|
-
mkdir -p "$record_dir" 2>/dev/null || return 0
|
|
1557
|
-
tmp="$(mktemp "$record_dir/.${AIUR_INSTANCE_KEY:-aiur}.XXXXXX" 2>/dev/null)" || return 0
|
|
1558
|
-
{
|
|
1559
|
-
printf 'AIUR_RECORD_NODE=%q\n' "$AIUR_RELEASE_NODE"
|
|
1560
|
-
printf 'AIUR_RECORD_INSTANCE_KEY=%q\n' "$AIUR_INSTANCE_KEY"
|
|
1561
|
-
printf 'AIUR_RECORD_SESSION=%q\n' "$session"
|
|
1562
|
-
printf 'AIUR_RECORD_SOCKET=%q\n' "$socket"
|
|
1563
|
-
printf 'AIUR_RECORD_AGENT_TMPFILE=%q\n' "${AIUR_AGENT_TMPFILE:-}"
|
|
1564
|
-
printf 'AIUR_RECORD_WORKSPACE_ROOT_FILE=%q\n' "${AIUR_WORKSPACE_ROOT_FILE:-}"
|
|
1565
|
-
printf 'AIUR_RECORD_PROJECT_ROOT=%q\n' "$root"
|
|
1566
|
-
printf 'AIUR_RECORD_PROJECT_ROOT_SOURCE=%q\n' "${AIUR_PROJECT_ROOT_SOURCE:-}"
|
|
1567
|
-
printf 'AIUR_RECORD_WRITTEN_AT=%q\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || true)"
|
|
1568
|
-
} >"$tmp" || { rm -f "$tmp" 2>/dev/null || true; return 0; }
|
|
1569
|
-
chmod 0600 "$tmp" 2>/dev/null || true
|
|
1570
|
-
if [ "$write_mode" = "if-absent" ]; then
|
|
1571
|
-
# Same-directory hard-link creation is atomic: a concurrent live launcher
|
|
1572
|
-
# can win without an attaching shell overwriting its workspace handoff.
|
|
1573
|
-
ln "$tmp" "$record" 2>/dev/null || true
|
|
1574
|
-
rm -f "$tmp" 2>/dev/null || true
|
|
1575
|
-
else
|
|
1576
|
-
mv "$tmp" "$record" 2>/dev/null || rm -f "$tmp" 2>/dev/null || true
|
|
1577
|
-
fi
|
|
1578
|
-
}
|
|
1579
|
-
|
|
1580
|
-
# Marker `status` reads to tell "daemon crashed, agents may be orphaned" apart
|
|
1581
|
-
# from "nothing was ever running". Written by the watchdog on an unexpected exit,
|
|
1582
|
-
# cleared on a clean stop and on the next successful background start.
|
|
1583
|
-
aiur_crash_marker_path() {
|
|
1584
|
-
printf '%s/%s.last-crash' "${AIUR_BG_STATE_DIR:?}" "$(aiur_state_slug)"
|
|
1585
|
-
}
|
|
1586
|
-
|
|
1587
|
-
# Sentinel `stop` drops before killing the BEAM so the watchdog knows the exit
|
|
1588
|
-
# was intentional and does not record a false crash.
|
|
1589
|
-
aiur_stop_sentinel_path() {
|
|
1590
|
-
printf '%s/%s.stopping' "${AIUR_BG_STATE_DIR:?}" "$(aiur_state_slug)"
|
|
1591
|
-
}
|
|
1592
|
-
|
|
1593
|
-
json_escape_alert_field() {
|
|
1594
|
-
LC_ALL=C printf '%s' "$1" | tr -d '\000-\037' | sed 's/\\/\\\\/g; s/"/\\"/g'
|
|
1595
|
-
}
|
|
1596
|
-
|
|
1597
|
-
crash_dump_identity() {
|
|
1598
|
-
local dump_path="${1:-}"
|
|
1599
|
-
[ -n "$dump_path" ] && [ -f "$dump_path" ] || return 0
|
|
1600
|
-
# Constant-time and replacement-sensitive. GNU stat supplies nanosecond
|
|
1601
|
-
# timestamps; BSD/macOS stat still includes device + inode, so an atomic
|
|
1602
|
-
# replacement with byte-identical contents cannot match the launch baseline.
|
|
1603
|
-
stat -Lc '%d:%i:%s:%y:%z' "$dump_path" 2>/dev/null || \
|
|
1604
|
-
stat -f '%d:%i:%z:%m:%c:%B' "$dump_path" 2>/dev/null || true
|
|
1605
|
-
}
|
|
1606
|
-
|
|
1607
|
-
new_completed_crash_dump_slogan() {
|
|
1608
|
-
local dump_path="${1:-}" baseline_file="${2:-}" baseline="" current_identity="" slogan=""
|
|
1609
|
-
[ -n "$baseline_file" ] && [ -r "$baseline_file" ] && IFS= read -r baseline <"$baseline_file" || true
|
|
1610
|
-
current_identity="$(crash_dump_identity "$dump_path")"
|
|
1611
|
-
[ -n "$current_identity" ] && [ "$current_identity" != "$baseline" ] || return 0
|
|
1612
|
-
tail -n 1 "$dump_path" 2>/dev/null | grep -q '^=end$' || return 0
|
|
1613
|
-
slogan="$(LC_ALL=C awk '/^Slogan: / { sub(/^Slogan: /, ""); print substr($0, 1, 512); exit }' "$dump_path" 2>/dev/null || true)"
|
|
1614
|
-
LC_ALL=C printf '%s' "$slogan" | tr -d '\000-\037'
|
|
1615
|
-
}
|
|
1616
|
-
|
|
1617
|
-
write_crash_dump_alert() {
|
|
1618
|
-
local ledger_path_file="$1" timestamp="$2" dump_path="$3" slogan="$4" ledger_path=""
|
|
1619
|
-
local escaped_timestamp escaped_path escaped_slogan escaped_message escaped_reason
|
|
1620
|
-
[ -n "$ledger_path_file" ] && [ -s "$ledger_path_file" ] || return 0
|
|
1621
|
-
# File.write/2 handoffs are valid without a trailing newline. `read` returns
|
|
1622
|
-
# nonzero at EOF in that case but still assigns the complete path.
|
|
1623
|
-
IFS= read -r ledger_path <"$ledger_path_file" || true
|
|
1624
|
-
[ -n "$ledger_path" ] || return 0
|
|
1625
|
-
|
|
1626
|
-
escaped_timestamp="$(json_escape_alert_field "$timestamp")"
|
|
1627
|
-
escaped_path="$(json_escape_alert_field "$dump_path")"
|
|
1628
|
-
escaped_slogan="$(json_escape_alert_field "$slogan")"
|
|
1629
|
-
escaped_message="$(json_escape_alert_field "BEAM crash dump: $slogan")"
|
|
1630
|
-
escaped_reason="$(json_escape_alert_field "Unexpected daemon exit wrote $dump_path")"
|
|
1631
|
-
|
|
1632
|
-
mkdir -p "$(dirname "$ledger_path")" 2>/dev/null || return 0
|
|
1633
|
-
printf '{"event":"alert","agent":"system","timestamp":"%s","name":"system.beam.crash_dump","topic":"system.beam.crash_dump","message":"%s","reason":"%s","severity":"warning","needs_attention":true,"dump_path":"%s","slogan":"%s"}\n' \
|
|
1634
|
-
"$escaped_timestamp" "$escaped_message" "$escaped_reason" "$escaped_path" "$escaped_slogan" \
|
|
1635
|
-
>>"$ledger_path" 2>/dev/null || true
|
|
1636
|
-
}
|
|
1637
|
-
|
|
1638
|
-
record_new_crash_dump_alert() {
|
|
1639
|
-
local ledger_path_file="${1:-}" baseline_file="${2:-}" crash_dump_path="${ERL_CRASH_DUMP:-}"
|
|
1640
|
-
local crash_dump_slogan="" ts=""
|
|
1641
|
-
crash_dump_slogan="$(new_completed_crash_dump_slogan "$crash_dump_path" "$baseline_file")"
|
|
1642
|
-
[ -n "$crash_dump_slogan" ] || return 0
|
|
1643
|
-
ts="$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || true)"
|
|
1644
|
-
write_crash_dump_alert "$ledger_path_file" "${ts:-unknown}" "$crash_dump_path" "$crash_dump_slogan"
|
|
1645
|
-
}
|
|
1646
|
-
|
|
1647
|
-
# Write a durable record that the background BEAM exited unexpectedly. Two sinks:
|
|
1648
|
-
# the run log dir (full record next to aiur.log, for forensics) and the stable
|
|
1649
|
-
# per-instance marker (for `status` to surface). Best-effort throughout — this
|
|
1650
|
-
# runs in the disowned watchdog after the BEAM is already gone, so it must never
|
|
1651
|
-
# fail loudly or block the reap that follows.
|
|
1652
|
-
record_beam_crash() {
|
|
1653
|
-
local node="$1" run_log_dir="$2" marker="$3" ledger_path_file="${4:-}" baseline_file="${5:-}"
|
|
1654
|
-
local ts boot_tail="" crash_dump_path="" crash_dump_slogan=""
|
|
1655
|
-
ts="$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || true)"
|
|
1656
|
-
if [ -n "$run_log_dir" ] && [ -r "$run_log_dir/log/boot.out.log" ]; then
|
|
1657
|
-
boot_tail="$(tail -n 20 "$run_log_dir/log/boot.out.log" 2>/dev/null || true)"
|
|
1658
|
-
fi
|
|
1659
|
-
crash_dump_path="${ERL_CRASH_DUMP:-}"
|
|
1660
|
-
crash_dump_slogan="$(new_completed_crash_dump_slogan "$crash_dump_path" "$baseline_file")"
|
|
1661
|
-
|
|
1662
|
-
local body
|
|
1663
|
-
body="$(
|
|
1664
|
-
printf 'aiur background BEAM exited unexpectedly\n'
|
|
1665
|
-
printf 'timestamp: %s\n' "${ts:-unknown}"
|
|
1666
|
-
printf 'node: %s\n' "${node:-unknown}"
|
|
1667
|
-
printf 'run_log_dir: %s\n' "${run_log_dir:-unknown}"
|
|
1668
|
-
printf 'detected_by: background BEAM-death watchdog (no clean stop sentinel)\n'
|
|
1669
|
-
if [ -n "$crash_dump_slogan" ]; then
|
|
1670
|
-
printf 'crash_dump_path: %s\n' "$crash_dump_path"
|
|
1671
|
-
printf 'crash_dump_slogan: %s\n' "$crash_dump_slogan"
|
|
1672
|
-
fi
|
|
1673
|
-
if [ -n "$boot_tail" ]; then
|
|
1674
|
-
printf -- '--- last 20 lines of boot.out.log ---\n%s\n' "$boot_tail"
|
|
1675
|
-
fi
|
|
1676
|
-
)"
|
|
1677
|
-
|
|
1678
|
-
if [ -n "$run_log_dir" ]; then
|
|
1679
|
-
mkdir -p "$run_log_dir/log" 2>/dev/null || true
|
|
1680
|
-
printf '%s\n' "$body" >>"$run_log_dir/log/aiur.crash" 2>/dev/null || true
|
|
1681
|
-
fi
|
|
1682
|
-
if [ -n "$marker" ]; then
|
|
1683
|
-
printf '%s\n' "$body" >"$marker" 2>/dev/null || true
|
|
1684
|
-
fi
|
|
1685
|
-
if [ -n "$crash_dump_slogan" ]; then
|
|
1686
|
-
write_crash_dump_alert "$ledger_path_file" "${ts:-unknown}" "$crash_dump_path" "$crash_dump_slogan"
|
|
1687
|
-
fi
|
|
1688
|
-
}
|
|
1689
|
-
|
|
1690
|
-
canonical_workspace_root() {
|
|
1691
|
-
local root="$1"
|
|
1692
|
-
# The BEAM hands off Config.workspace_root() verbatim, which may be a `~/...`
|
|
1693
|
-
# path (e.g. this repo's `workspace.root: ~/code/aiur-workspaces`). `cd` does
|
|
1694
|
-
# not expand `~` inside a quoted variable, so without this a `~`-rooted cwd
|
|
1695
|
-
# sweep compared against the literal `~/...` string, matched no /proc cwd, and
|
|
1696
|
-
# silently reaped nothing — the exact backstop that must catch workspace-rooted
|
|
1697
|
-
# agents on stop. Expand a leading `~` before canonicalizing so the sweep and
|
|
1698
|
-
# the /proc cwd values (always absolute) agree.
|
|
1699
|
-
case "$root" in
|
|
1700
|
-
"~" | "~/"*) root="${HOME:-}${root#\~}" ;;
|
|
1701
|
-
esac
|
|
1702
|
-
if [ -d "$root" ]; then
|
|
1703
|
-
(cd "$root" 2>/dev/null && pwd -P) || printf '%s\n' "$root"
|
|
1704
|
-
else
|
|
1705
|
-
printf '%s\n' "$root"
|
|
1706
|
-
fi
|
|
1707
|
-
}
|
|
1708
|
-
|
|
1709
|
-
workspace_root_is_shallow() {
|
|
1710
|
-
local root="${1%/}" trimmed
|
|
1711
|
-
trimmed="${root#/}"
|
|
1712
|
-
[ -n "$trimmed" ] || return 0
|
|
1713
|
-
case "$trimmed" in
|
|
1714
|
-
*/*) return 1 ;;
|
|
1715
|
-
*) return 0 ;;
|
|
1716
|
-
esac
|
|
1717
|
-
}
|
|
1718
|
-
|
|
1719
|
-
workspace_cwd_pids() {
|
|
1720
|
-
local root="${1%/}" proc_dir="${2:-/proc}" entry pid cwd
|
|
1721
|
-
[ -d "$proc_dir" ] || return 0
|
|
1722
|
-
|
|
1723
|
-
for entry in "$proc_dir"/[0-9]*; do
|
|
1724
|
-
[ -d "$entry" ] || continue
|
|
1725
|
-
pid="${entry##*/}"
|
|
1726
|
-
cwd="$(readlink "$entry/cwd" 2>/dev/null || true)"
|
|
1727
|
-
[ -n "$cwd" ] || continue
|
|
1728
|
-
cwd="${cwd%/}"
|
|
1729
|
-
if [ "$cwd" != "$root" ] && [[ "$cwd/" == "$root/"* ]]; then
|
|
1730
|
-
printf '%s\n' "$pid"
|
|
1731
|
-
fi
|
|
1732
|
-
done
|
|
1733
|
-
}
|
|
1734
|
-
|
|
1735
|
-
shell_protected_pid_list() {
|
|
1736
|
-
local p
|
|
1737
|
-
printf '%s\n' "$$"
|
|
1738
|
-
[ "${BASHPID:-$$}" = "$$" ] || printf '%s\n' "$BASHPID"
|
|
1739
|
-
[ -n "${PPID:-}" ] && printf '%s\n' "$PPID"
|
|
1740
|
-
for p in $(agent_pid_tree "$$" 2>/dev/null || true); do
|
|
1741
|
-
printf '%s\n' "$p"
|
|
1742
|
-
done
|
|
1743
|
-
}
|
|
1744
|
-
|
|
1745
|
-
reap_workspace_cwd_agents() {
|
|
1746
|
-
local root="${1:-}"
|
|
1747
|
-
[ -n "$root" ] || return 0
|
|
1748
|
-
[ -d /proc ] || return 0
|
|
1749
|
-
|
|
1750
|
-
root="$(canonical_workspace_root "$root")"
|
|
1751
|
-
if workspace_root_is_shallow "$root"; then
|
|
1752
|
-
echo "⚠️ refusing shallow workspace cwd sweep root: $root" >&2
|
|
1753
|
-
return 0
|
|
1754
|
-
fi
|
|
1755
|
-
|
|
1756
|
-
local max_sweeps="${AIUR_WORKSPACE_REAP_SWEEPS:-6}"
|
|
1757
|
-
case "$max_sweeps" in '' | *[!0-9]*) max_sweeps=6 ;; esac
|
|
1758
|
-
[ "$max_sweeps" -gt 0 ] || return 0
|
|
1759
|
-
|
|
1760
|
-
local protected pids=() filtered=() pid p sweep waited any
|
|
1761
|
-
protected=" $(shell_protected_pid_list | tr '\n' ' ') "
|
|
1762
|
-
|
|
1763
|
-
sweep=0
|
|
1764
|
-
while [ "$sweep" -lt "$max_sweeps" ]; do
|
|
1765
|
-
mapfile -t pids < <(workspace_cwd_pids "$root" /proc)
|
|
1766
|
-
filtered=()
|
|
1767
|
-
for pid in "${pids[@]}"; do
|
|
1768
|
-
case "$protected" in *" $pid "*) continue ;; esac
|
|
1769
|
-
kill -0 "$pid" 2>/dev/null && filtered+=("$pid")
|
|
1770
|
-
done
|
|
1771
|
-
|
|
1772
|
-
[ "${#filtered[@]}" -gt 0 ] || return 0
|
|
1773
|
-
|
|
1774
|
-
for p in "${filtered[@]}"; do kill -TERM "$p" 2>/dev/null || true; done
|
|
1775
|
-
waited=0
|
|
1776
|
-
while [ "$waited" -lt 10 ]; do
|
|
1777
|
-
any=0
|
|
1778
|
-
for p in "${filtered[@]}"; do kill -0 "$p" 2>/dev/null && { any=1; break; }; done
|
|
1779
|
-
[ "$any" -eq 0 ] && break
|
|
1780
|
-
sleep 0.1
|
|
1781
|
-
waited=$((waited + 1))
|
|
1782
|
-
done
|
|
1783
|
-
for p in "${filtered[@]}"; do kill -KILL "$p" 2>/dev/null || true; done
|
|
1784
|
-
|
|
1785
|
-
sweep=$((sweep + 1))
|
|
1786
|
-
sleep 0.1
|
|
1787
|
-
done
|
|
1788
|
-
}
|
|
1789
|
-
|
|
1790
|
-
reap_workspace_cwd_from_file() {
|
|
1791
|
-
local root_file="${1:-}" root
|
|
1792
|
-
[ -n "$root_file" ] && [ -s "$root_file" ] || return 0
|
|
1793
|
-
IFS= read -r root <"$root_file" || root=""
|
|
1794
|
-
reap_workspace_cwd_agents "$root"
|
|
1795
|
-
}
|
|
1796
|
-
|
|
1797
|
-
workspace_root_file_from_instance_record() {
|
|
1798
|
-
load_aiur_instance_record "$(aiur_instance_record_path)" || return 1
|
|
1799
|
-
[ -n "${AIUR_RECORD_WORKSPACE_ROOT_FILE:-}" ] || return 1
|
|
1800
|
-
printf '%s\n' "$AIUR_RECORD_WORKSPACE_ROOT_FILE"
|
|
1801
|
-
}
|
|
1802
|
-
|
|
1803
|
-
agent_pidfile_from_instance_record() {
|
|
1804
|
-
load_aiur_instance_record "$(aiur_instance_record_path)" || return 1
|
|
1805
|
-
if [ -n "${AIUR_RECORD_AGENT_TMPFILE:-}" ]; then
|
|
1806
|
-
printf '%s\n' "$AIUR_RECORD_AGENT_TMPFILE"
|
|
1807
|
-
return 0
|
|
1808
|
-
fi
|
|
1809
|
-
|
|
1810
|
-
# Records written before the pidfile field still carry the handoff created
|
|
1811
|
-
# by the same launcher: aiur-PID-workspace-root and aiur-PID-agents share a
|
|
1812
|
-
# runtime directory. Derive that one file only when the old path has the
|
|
1813
|
-
# exact launch shape under this runtime root; never scan other instances.
|
|
1814
|
-
local session_root="${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}" handoff pid
|
|
1815
|
-
session_root="${session_root%/}"
|
|
1816
|
-
[ -n "$session_root" ] || session_root=/
|
|
1817
|
-
handoff="${AIUR_RECORD_WORKSPACE_ROOT_FILE:-}"
|
|
1818
|
-
case "$handoff" in
|
|
1819
|
-
"$session_root"/aiur-*-workspace-root) ;;
|
|
1820
|
-
*) return 1 ;;
|
|
1821
|
-
esac
|
|
1822
|
-
pid="${handoff#"$session_root"/aiur-}"
|
|
1823
|
-
pid="${pid%-workspace-root}"
|
|
1824
|
-
[[ "$pid" =~ ^[0-9]+$ ]] || return 1
|
|
1825
|
-
printf '%s/aiur-%s-agents\n' "$session_root" "$pid"
|
|
1826
|
-
}
|
|
1827
|
-
|
|
1828
|
-
# Background watchdog that survives the BEAM. Polls for the release BEAM by
|
|
1829
|
-
# command pattern and, once it has SEEN the BEAM and then the BEAM disappears
|
|
1830
|
-
# (orderly halt OR :emfile-style crash), reaps every agent. Polling the pattern
|
|
1831
|
-
# rather than a captured pid avoids two failure modes: a recycled BEAM pid the
|
|
1832
|
-
# watchdog would poll forever, and an empty pid at arm time that would silently
|
|
1833
|
-
# disarm the only crash reaper. The `seen` latch prevents a startup race from
|
|
1834
|
-
# reaping before the BEAM has come up. kill-server collapses the orphaned tmux
|
|
1835
|
-
# session, which returns the foreground `tmux attach` so the EXIT trap
|
|
1836
|
-
# (session_cleanup) runs its idempotent reap too.
|
|
1837
|
-
#
|
|
1838
|
-
# $1 beam_pattern $2 socket $3 pidfile $4 interval_s $5 initial_seen
|
|
1839
|
-
# $6 node $7 run_log_dir $8 stop_sentinel $9 crash_marker
|
|
1840
|
-
# $10 workspace_root_file $11 alert_ledger_path_file $12 dump_baseline_file
|
|
1841
|
-
# Any of args 6-9 or 11 arm crash recording. Foreground supplies only the node
|
|
1842
|
-
# and alert-ledger handoff, so it can alert on a new dump without writing the
|
|
1843
|
-
# background-only run marker.
|
|
1844
|
-
# Prints the watchdog's own pid so the caller can kill it on a clean teardown.
|
|
1845
|
-
start_beam_death_watchdog() {
|
|
1846
|
-
local beam_pattern="$1" socket="$2" pidfile="$3" interval="${4:-1}" initial_seen="${5:-0}"
|
|
1847
|
-
local node="${6:-}" run_log_dir="${7:-}" stop_sentinel="${8:-}" crash_marker="${9:-}" workspace_root_file="${10:-}"
|
|
1848
|
-
local alert_ledger_path_file="${11:-}" dump_baseline_file="${12:-}"
|
|
1849
|
-
# Redirect the subshell's stdout so a command-substitution caller
|
|
1850
|
-
# (`pid=$(start_beam_death_watchdog ...)`) returns immediately instead of
|
|
1851
|
-
# blocking on the still-open pipe until the watchdog finishes.
|
|
1852
|
-
(
|
|
1853
|
-
seen="$initial_seen"
|
|
1854
|
-
while :; do
|
|
1855
|
-
if pgrep -f -- "$beam_pattern" >/dev/null 2>&1; then
|
|
1856
|
-
seen=1
|
|
1857
|
-
elif [ "$seen" = 1 ]; then
|
|
1858
|
-
break
|
|
1859
|
-
fi
|
|
1860
|
-
sleep "$interval"
|
|
1861
|
-
done
|
|
1862
|
-
# The BEAM we had seen is gone. A clean `aiur stop` drops the sentinel first;
|
|
1863
|
-
# its presence means an intentional exit (consume it, no crash record).
|
|
1864
|
-
# Anything else is an unexpected exit worth a durable record before reaping.
|
|
1865
|
-
if [ -n "$stop_sentinel" ] && [ -f "$stop_sentinel" ]; then
|
|
1866
|
-
rm -f "$stop_sentinel" 2>/dev/null || true
|
|
1867
|
-
elif [ -n "$crash_marker" ] || [ -n "$run_log_dir" ] || [ -n "$alert_ledger_path_file" ]; then
|
|
1868
|
-
record_beam_crash "$node" "$run_log_dir" "$crash_marker" "$alert_ledger_path_file" "$dump_baseline_file"
|
|
1869
|
-
fi
|
|
1870
|
-
reap_aiur_agents "$socket" "$pidfile"
|
|
1871
|
-
reap_workspace_cwd_from_file "$workspace_root_file"
|
|
1872
|
-
rm -f "$alert_ledger_path_file" "$dump_baseline_file" 2>/dev/null || true
|
|
1873
|
-
) >/dev/null 2>&1 &
|
|
1874
|
-
printf '%s\n' "$!"
|
|
1875
|
-
}
|
|
1876
|
-
|
|
1877
|
-
probe_control_liveness() {
|
|
1878
|
-
local expression output status
|
|
1879
|
-
# The Orchestrator starts before the dashboard child, so an answering status
|
|
1880
|
-
# call alone does not prove startup (or the bind attempt) finished. Wait until
|
|
1881
|
-
# OTP marks :aiur started before printing the effective dashboard status.
|
|
1882
|
-
expression='aiur_started = Enum.any?(Application.started_applications(), fn {app, _description, _version} -> app == :aiur end); case {Process.whereis(Aiur.Orchestrator), aiur_started} do {pid, true} when is_pid(pid) -> case Aiur.Orchestrator.status(Aiur.Orchestrator, 100) do statuses when is_list(statuses) -> IO.puts("__AIUR_CONTROL_READY__"); _ -> IO.puts("__AIUR_CONTROL_NOT_READY__") end; _ -> IO.puts("__AIUR_CONTROL_NOT_READY__") end'
|
|
1883
|
-
|
|
1884
|
-
set +e
|
|
1885
|
-
output="$("$release_bin" rpc "$expression" 2>&1)"
|
|
1886
|
-
status=$?
|
|
1887
|
-
set -e
|
|
1888
|
-
|
|
1889
|
-
if [ "$status" -eq 0 ] && [[ "$output" == *"__AIUR_CONTROL_READY__"* ]]; then
|
|
1890
|
-
printf 'up'
|
|
1891
|
-
else
|
|
1892
|
-
printf 'down'
|
|
1893
|
-
fi
|
|
1894
|
-
}
|
|
1895
|
-
|
|
1896
|
-
# Return the useful URL and effective bind host/port only when the running node
|
|
1897
|
-
# confirms that Bandit actually bound a listener. An empty result means the
|
|
1898
|
-
# listener was suppressed or refused; configured server.port alone is never
|
|
1899
|
-
# proof of service.
|
|
1900
|
-
probe_dashboard_status() {
|
|
1901
|
-
local expression output status marker="__AIUR_DASHBOARD_STATUS__:"
|
|
1902
|
-
expression='case {Aiur.HttpServer.base_url(), Aiur.Config.server_host(), Aiur.HttpServer.bound_port()} do {url, host, port} when is_binary(url) and is_binary(host) and is_integer(port) -> IO.puts("__AIUR_DASHBOARD_STATUS__:Dashboard: " <> url <> " (bind host=" <> host <> ", port=" <> Integer.to_string(port) <> ")"); _ -> :ok end'
|
|
1903
|
-
|
|
1904
|
-
set +e
|
|
1905
|
-
run_release_rpc_with_timeout "$expression"
|
|
1906
|
-
status=$?
|
|
1907
|
-
set -e
|
|
1908
|
-
output="$AIUR_CONTROL_RPC_OUTPUT"
|
|
1909
|
-
|
|
1910
|
-
if [ "$status" -eq 0 ] && [[ "$output" == *"$marker"* ]]; then
|
|
1911
|
-
output="${output#*"$marker"}"
|
|
1912
|
-
printf '%s' "${output%%$'\n'*}"
|
|
1913
|
-
fi
|
|
1914
|
-
}
|
|
1915
|
-
|
|
1916
|
-
# Replay the exact path selected by the CLI inside tmux. The BEAM's startup
|
|
1917
|
-
# output is captured rather than shown directly, so the marker crosses that
|
|
1918
|
-
# boundary without duplicating config-discovery rules in this launcher.
|
|
1919
|
-
print_config_status() {
|
|
1920
|
-
local startup_capture="$1" marker="__AIUR_CONFIG_PATH__:" config_path
|
|
1921
|
-
|
|
1922
|
-
config_path="$(awk -v marker="$marker" 'index($0, marker) == 1 { print substr($0, length(marker) + 1); exit }' "$startup_capture" 2>/dev/null || true)"
|
|
1923
|
-
|
|
1924
|
-
if [ -n "$config_path" ]; then
|
|
1925
|
-
echo "Config: ${config_path}" >&2
|
|
1926
|
-
else
|
|
1927
|
-
echo "⚠️ selected config path unavailable in captured startup output." >&2
|
|
1928
|
-
fi
|
|
1929
|
-
}
|
|
1930
|
-
|
|
1931
|
-
print_dashboard_status() {
|
|
1932
|
-
local no_dashboard="$1" startup_capture="$2" dashboard_status
|
|
1933
|
-
|
|
1934
|
-
if [ "$no_dashboard" -eq 1 ]; then
|
|
1935
|
-
echo "Dashboard disabled by --no-dashboard." >&2
|
|
1936
|
-
return
|
|
1937
|
-
fi
|
|
1938
|
-
|
|
1939
|
-
dashboard_status="$(probe_dashboard_status)"
|
|
1940
|
-
if [ -n "$dashboard_status" ]; then
|
|
1941
|
-
echo "${dashboard_status}" >&2
|
|
1942
|
-
else
|
|
1943
|
-
echo "⚠️ dashboard listener unavailable; inspect ${startup_capture} for bind or authentication refusal." >&2
|
|
1944
|
-
fi
|
|
1945
|
-
}
|
|
1946
|
-
|
|
1947
|
-
wait_for_session_startup() {
|
|
1948
|
-
local tmux_bin="$1" socket="$2" conf="$3" session="$4" startup_capture="$5" require_control="$6"
|
|
1949
|
-
local max_ticks="${AIUR_TMUX_GRACE_TICKS:-30}" tick control_state
|
|
1950
|
-
if [ "$require_control" = "1" ]; then
|
|
1951
|
-
# 0.1s per tick. The old 100-tick (10s) budget was under a cold control
|
|
1952
|
-
# plane's real boot time, so a healthy daemon was killed mid-startup and
|
|
1953
|
-
# reported as a failure. Boot time grows with the module count; keep the
|
|
1954
|
-
# headroom generous, since the loop exits as soon as the probe says "up".
|
|
1955
|
-
max_ticks="${AIUR_NODE_GRACE_TICKS:-1200}"
|
|
1956
|
-
fi
|
|
1957
|
-
|
|
1958
|
-
for ((tick = 0; tick < max_ticks; tick++)); do
|
|
1959
|
-
if ! "$tmux_bin" -L "$socket" -f "$conf" has-session -t "$session" 2>/dev/null; then
|
|
1960
|
-
echo "❌ aiur exited during startup; captured output:" >&2
|
|
1961
|
-
tail -n 30 "$startup_capture" 2>/dev/null | sed 's/^/ /' >&2 || true
|
|
1962
|
-
return 1
|
|
1963
|
-
fi
|
|
1964
|
-
|
|
1965
|
-
if [ "$require_control" != "1" ]; then
|
|
1966
|
-
sleep 0.1
|
|
1967
|
-
continue
|
|
1968
|
-
fi
|
|
1969
|
-
|
|
1970
|
-
control_state="$(probe_control_liveness)"
|
|
1971
|
-
if [ "$control_state" = "up" ]; then
|
|
1972
|
-
return 0
|
|
1973
|
-
fi
|
|
1974
|
-
|
|
1975
|
-
sleep 0.1
|
|
1976
|
-
done
|
|
1977
|
-
|
|
1978
|
-
if [ "$require_control" = "1" ]; then
|
|
1979
|
-
# A live session at this point means the BEAM never died -- we ran out of
|
|
1980
|
-
# patience, not the daemon out of health. Saying so is the difference
|
|
1981
|
-
# between "your release is broken" and "give it longer", and the empty
|
|
1982
|
-
# capture below is evidence of the latter, not of a silent crash.
|
|
1983
|
-
if "$tmux_bin" -L "$socket" -f "$conf" has-session -t "$session" 2>/dev/null; then
|
|
1984
|
-
echo "❌ aiur control plane at ${RELEASE_NODE:-unknown} was still booting after $(awk "BEGIN{printf \"%.0f\", ${max_ticks} / 10}")s; the BEAM is alive but not yet answering." >&2
|
|
1985
|
-
echo " Raise the budget with AIUR_NODE_GRACE_TICKS (ticks of 0.1s) if this box is just slow." >&2
|
|
1986
|
-
else
|
|
1987
|
-
echo "❌ aiur control plane did not become ready at ${RELEASE_NODE:-unknown} during startup; captured output:" >&2
|
|
1988
|
-
fi
|
|
1989
|
-
tail -n 30 "$startup_capture" 2>/dev/null | sed 's/^/ /' >&2 || true
|
|
1990
|
-
return 1
|
|
1991
|
-
fi
|
|
1992
|
-
|
|
1993
|
-
return 0
|
|
1994
|
-
}
|
|
1995
|
-
|
|
1996
|
-
# Foreground teardown: kill the session + BEAM and reap opencode sessions on exit.
|
|
1997
|
-
_session_socket="" _session_name="" _session_conf="" _session_tmpfile=""
|
|
1998
|
-
_session_capture="" _session_argv="" _session_release="" _session_tmux="" _session_node=""
|
|
1999
|
-
_session_pidfile="" _session_watchdog_pid="" _session_workspace_root_file=""
|
|
2000
|
-
_session_alert_ledger_path_file="" _session_crash_dump_baseline_file="" _session_launch_lock=""
|
|
2001
|
-
_cleanup_ran=0
|
|
2002
|
-
session_cleanup() {
|
|
2003
|
-
[ "$_cleanup_ran" = 1 ] && return 0
|
|
2004
|
-
_cleanup_ran=1
|
|
2005
|
-
local code=$?
|
|
2006
|
-
|
|
2007
|
-
# Stop the BEAM-death watchdog: cleanup is running, so it has no work left and
|
|
2008
|
-
# a lingering poller would outlive this teardown.
|
|
2009
|
-
[ -n "$_session_watchdog_pid" ] && kill "$_session_watchdog_pid" 2>/dev/null || true
|
|
2010
|
-
|
|
2011
|
-
# kill-session on the detached aiur session FIRST: this is what propagates
|
|
2012
|
-
# SIGHUP to the BEAM in that session so it begins its own orderly shutdown.
|
|
2013
|
-
# Without it the BEAM survives Ctrl+C, holding port 4000 + the node name and
|
|
2014
|
-
# breaking the next launch (regression test/aiur/regression/shutdown_cleanup_test.exs).
|
|
2015
|
-
if [ -n "$_session_tmux" ] && [ -n "$_session_socket" ] && [ -n "$_session_name" ]; then
|
|
2016
|
-
"$_session_tmux" -L "$_session_socket" kill-session -t "$_session_name" 2>/dev/null || true
|
|
2017
|
-
fi
|
|
2018
|
-
|
|
2019
|
-
# Then kill-server on aiur's private socket: tears down every pane agent across
|
|
2020
|
-
# all windows AND leaves no live aiur tmux server, then reaps headless agents
|
|
2021
|
-
# from the pidfile. Idempotent with the watchdog's own reap and the kill-session
|
|
2022
|
-
# above. reap_aiur_agents re-resolves tmux itself and reaps headless agents even
|
|
2023
|
-
# when tmux is absent, so it is not gated on $_session_tmux.
|
|
2024
|
-
reap_aiur_agents "$_session_socket" "$_session_pidfile"
|
|
2025
|
-
|
|
2026
|
-
# Reap only this instance's BEAM. Multiple keyed instances can share the same
|
|
2027
|
-
# release dir, so release-path pgrep would terminate sibling workflows. Give
|
|
2028
|
-
# the BEAM a generous TERM grace (the stop grace, not the 3s startup reclaim
|
|
2029
|
-
# default) so its own graceful shutdown — ProcessReaper reaping the agent tree
|
|
2030
|
-
# plus the BEAM-side workspace sweep — completes before any SIGKILL lands.
|
|
2031
|
-
[ -n "$_session_node" ] && kill_beams_matching "-name ${_session_node}" 300
|
|
2032
|
-
|
|
2033
|
-
# Final cwd-sweep backstop after the BEAM is gone: catches workspace-rooted
|
|
2034
|
-
# agents or test children that registered too late, reparented during the
|
|
2035
|
-
# BEAM-side sweep, or survived a bounded in-BEAM cleanup.
|
|
2036
|
-
reap_workspace_cwd_from_file "$_session_workspace_root_file"
|
|
2037
|
-
|
|
2038
|
-
# Reap the epmd our BEAM spawned for distribution so it doesn't linger after
|
|
2039
|
-
# exit. The node is dead by now, so `epmd -kill` succeeds; it safely refuses
|
|
2040
|
-
# while any *other* Erlang node is alive, so a daemon shared with an unrelated
|
|
2041
|
-
# Erlang program is never disrupted. Use the release's bundled epmd and pin it
|
|
2042
|
-
# to the loopback address aiur starts it on.
|
|
2043
|
-
for _epmd in "$_session_release"/erts-*/bin/epmd; do
|
|
2044
|
-
[ -x "$_epmd" ] || continue
|
|
2045
|
-
ERL_EPMD_ADDRESS="${ERL_EPMD_ADDRESS:-127.0.0.1}" "$_epmd" -kill >/dev/null 2>&1 || true
|
|
2046
|
-
done
|
|
2047
|
-
|
|
2048
|
-
if command -v opencode >/dev/null 2>&1 && [ -s "$_session_tmpfile" ]; then
|
|
2049
|
-
local id
|
|
2050
|
-
while IFS= read -r id; do
|
|
2051
|
-
[ -n "$id" ] || continue
|
|
2052
|
-
opencode session delete "$id" >/dev/null 2>&1 &
|
|
2053
|
-
done <"$_session_tmpfile"
|
|
2054
|
-
wait 2>/dev/null || true
|
|
2055
|
-
fi
|
|
2056
|
-
|
|
2057
|
-
# Reap agent-driver sockets this run orphaned at close, not next launch.
|
|
2058
|
-
sweep_dead_tmux_sockets || true
|
|
2059
|
-
|
|
2060
|
-
# Reap stale aiur /tmp debris (per-run tempfiles, leaked test artifacts) so a
|
|
2061
|
-
# tmpfs /tmp can't fill across runs. Age-gated + pid-safe; our own still-present
|
|
2062
|
-
# tempfiles are spared (fresh + live pid) before the explicit rm below clears them.
|
|
2063
|
-
sweep_stale_tmp_artifacts || true
|
|
2064
|
-
|
|
2065
|
-
rm -f "$_session_tmpfile" "$_session_capture" "$_session_argv" "$_session_pidfile" \
|
|
2066
|
-
"$_session_workspace_root_file" "$_session_alert_ledger_path_file" \
|
|
2067
|
-
"$_session_crash_dump_baseline_file" 2>/dev/null || true
|
|
2068
|
-
rm -f "$(aiur_instance_record_path)" 2>/dev/null || true
|
|
2069
|
-
release_aiur_launch_lock "$_session_launch_lock"
|
|
2070
|
-
_session_launch_lock=""
|
|
2071
|
-
return $code
|
|
2072
|
-
}
|
|
2073
|
-
install_foreground_traps() {
|
|
2074
|
-
trap 'session_cleanup' EXIT
|
|
2075
|
-
trap 'trap - EXIT INT TERM HUP; session_cleanup; exit 130' INT
|
|
2076
|
-
trap 'trap - EXIT INT TERM HUP; session_cleanup; exit 143' TERM
|
|
2077
|
-
trap 'trap - EXIT INT TERM HUP; session_cleanup; exit 129' HUP
|
|
2078
|
-
}
|
|
2079
|
-
|
|
2080
|
-
# --- lifecycle (RPC into the running node + tmux ops) ------------------------
|
|
2081
|
-
|
|
2082
|
-
trim() {
|
|
2083
|
-
local s="$1"
|
|
2084
|
-
s="${s#"${s%%[![:space:]]*}"}"
|
|
2085
|
-
s="${s%"${s##*[![:space:]]}"}"
|
|
2086
|
-
printf '%s' "$s"
|
|
2087
|
-
}
|
|
2088
|
-
|
|
2089
|
-
# Classify our distribution node's liveness via epmd, which only advertises a
|
|
2090
|
-
# node while its BEAM holds the registration. Echoes exactly one word:
|
|
2091
|
-
# up — node is registered: it is alive, so an rpc failure is a real error
|
|
2092
|
-
# down — epmd answered and our node is absent: it genuinely is not running
|
|
2093
|
-
# unknown — epmd could not be queried (binary missing, or daemon unreachable),
|
|
2094
|
-
# so node state is indeterminate and callers must NOT assume "down"
|
|
2095
|
-
# and mask the real error
|
|
2096
|
-
# Distinguishing `unknown` from `down` matters: a live node whose epmd we cannot
|
|
2097
|
-
# reach must still surface its real rpc error rather than the "start aiur" hint.
|
|
2098
|
-
# Relies on RELEASE_NODE + ERL_EPMD_ADDRESS (prepare_distribution) and
|
|
2099
|
-
# release_dir (resolve_release), so call it only after both have run.
|
|
2100
|
-
probe_node_liveness() {
|
|
2101
|
-
local epmd names short="${RELEASE_NODE%@*}"
|
|
2102
|
-
for epmd in "$release_dir"/erts-*/bin/epmd; do
|
|
2103
|
-
[ -x "$epmd" ] || continue
|
|
2104
|
-
names="$(ERL_EPMD_ADDRESS="${ERL_EPMD_ADDRESS:-127.0.0.1}" "$epmd" -names 2>/dev/null)" \
|
|
2105
|
-
|| { printf 'unknown'; return; }
|
|
2106
|
-
case "$names" in
|
|
2107
|
-
*"name ${short} at port "*) printf 'up' ;;
|
|
2108
|
-
*) printf 'down' ;;
|
|
2109
|
-
esac
|
|
2110
|
-
return
|
|
2111
|
-
done
|
|
2112
|
-
printf 'unknown'
|
|
2113
|
-
}
|
|
2114
|
-
|
|
2115
|
-
confirm_stale_tmux_session() {
|
|
2116
|
-
local tmux_bin="$1" socket="$2" conf="$3" session="$4"
|
|
2117
|
-
local max_ticks="${AIUR_STALE_SESSION_GRACE_TICKS:-30}" tick
|
|
2118
|
-
|
|
2119
|
-
for ((tick = 0; tick < max_ticks; tick++)); do
|
|
2120
|
-
"$tmux_bin" -L "$socket" -f "$conf" has-session -t "$session" 2>/dev/null || return 0
|
|
2121
|
-
[ "$(probe_control_liveness)" = "up" ] && return 1
|
|
2122
|
-
[ "$(probe_node_liveness)" = "down" ] || return 1
|
|
2123
|
-
sleep 0.1
|
|
2124
|
-
done
|
|
2125
|
-
|
|
2126
|
-
"$tmux_bin" -L "$socket" -f "$conf" has-session -t "$session" 2>/dev/null || return 0
|
|
2127
|
-
[ "$(probe_control_liveness)" != "up" ] && [ "$(probe_node_liveness)" = "down" ]
|
|
2128
|
-
}
|
|
2129
|
-
|
|
2130
|
-
probe_named_node_liveness() {
|
|
2131
|
-
local node="$1" old_node="${RELEASE_NODE:-}" state
|
|
2132
|
-
RELEASE_NODE="$node"
|
|
2133
|
-
state="$(probe_node_liveness)"
|
|
2134
|
-
RELEASE_NODE="$old_node"
|
|
2135
|
-
printf '%s' "$state"
|
|
2136
|
-
}
|
|
2137
|
-
|
|
2138
|
-
load_aiur_instance_record() {
|
|
2139
|
-
local file="$1"
|
|
2140
|
-
AIUR_RECORD_NODE=""
|
|
2141
|
-
AIUR_RECORD_INSTANCE_KEY=""
|
|
2142
|
-
AIUR_RECORD_SESSION=""
|
|
2143
|
-
AIUR_RECORD_SOCKET=""
|
|
2144
|
-
AIUR_RECORD_AGENT_TMPFILE=""
|
|
2145
|
-
AIUR_RECORD_WORKSPACE_ROOT_FILE=""
|
|
2146
|
-
AIUR_RECORD_PROJECT_ROOT=""
|
|
2147
|
-
AIUR_RECORD_PROJECT_ROOT_SOURCE=""
|
|
2148
|
-
[ -r "$file" ] || return 1
|
|
2149
|
-
# Records are written by this engine as KEY=%q lines under the user's state dir.
|
|
2150
|
-
# shellcheck disable=SC1090
|
|
2151
|
-
source "$file" 2>/dev/null || return 1
|
|
2152
|
-
[ -n "$AIUR_RECORD_NODE" ] && [ -n "$AIUR_RECORD_SESSION" ] && \
|
|
2153
|
-
[ -n "$AIUR_RECORD_SOCKET" ] && [ -n "$AIUR_RECORD_PROJECT_ROOT" ]
|
|
2154
|
-
}
|
|
2155
|
-
|
|
2156
|
-
path_is_within_root() {
|
|
2157
|
-
local root child
|
|
2158
|
-
root="$(canonical_workspace_root "$1")"
|
|
2159
|
-
child="$(canonical_workspace_root "$2")"
|
|
2160
|
-
[ "$child" = "$root" ] || [[ "$child/" == "$root/"* ]]
|
|
2161
|
-
}
|
|
2162
|
-
|
|
2163
|
-
AIUR_CONTROL_ADOPTED_RECORD=0
|
|
2164
|
-
AIUR_CONTROL_CURRENT_NODE_STATE=""
|
|
2165
|
-
AIUR_CONTROL_HINT_ROOTS=""
|
|
2166
|
-
AIUR_CONTROL_CALLER_ROOT=""
|
|
2167
|
-
AIUR_CONTROL_CALLER_NODE=""
|
|
2168
|
-
AIUR_CONTROL_CALLER_ROOT_SOURCE=""
|
|
2169
|
-
|
|
2170
|
-
resolve_control_identity_from_records() {
|
|
2171
|
-
AIUR_CONTROL_ADOPTED_RECORD=0
|
|
2172
|
-
AIUR_CONTROL_CURRENT_NODE_STATE="$(probe_node_liveness)"
|
|
2173
|
-
AIUR_CONTROL_HINT_ROOTS=""
|
|
2174
|
-
AIUR_CONTROL_CALLER_ROOT="$(canonical_workspace_root "${AIUR_PROJECT_ROOT:-}")"
|
|
2175
|
-
AIUR_CONTROL_CALLER_NODE="$AIUR_RELEASE_NODE"
|
|
2176
|
-
AIUR_CONTROL_CALLER_ROOT_SOURCE="${AIUR_PROJECT_ROOT_SOURCE:-}"
|
|
2177
|
-
|
|
2178
|
-
[ "$AIUR_CONTROL_CURRENT_NODE_STATE" = "down" ] || return 0
|
|
2179
|
-
[ "${AIUR_PROJECT_ROOT_SOURCE:-}" = "cwd" ] || return 0
|
|
2180
|
-
|
|
2181
|
-
local dir file state root match_count=0
|
|
2182
|
-
local match_node="" match_key="" match_session="" match_socket="" match_root="" match_source=""
|
|
2183
|
-
dir="$(aiur_instances_dir)"
|
|
2184
|
-
[ -d "$dir" ] || return 0
|
|
2185
|
-
|
|
2186
|
-
for file in "$dir"/*.instance; do
|
|
2187
|
-
[ -e "$file" ] || continue
|
|
2188
|
-
load_aiur_instance_record "$file" || continue
|
|
2189
|
-
state="$(probe_named_node_liveness "$AIUR_RECORD_NODE")"
|
|
2190
|
-
[ "$state" = "up" ] || continue
|
|
2191
|
-
|
|
2192
|
-
root="$(canonical_workspace_root "$AIUR_RECORD_PROJECT_ROOT")"
|
|
2193
|
-
case "$AIUR_CONTROL_HINT_ROOTS" in
|
|
2194
|
-
*"
|
|
2195
|
-
$root
|
|
2196
|
-
"*) ;;
|
|
2197
|
-
*) AIUR_CONTROL_HINT_ROOTS="${AIUR_CONTROL_HINT_ROOTS}${root}
|
|
2198
|
-
" ;;
|
|
2199
|
-
esac
|
|
2200
|
-
|
|
2201
|
-
path_is_within_root "$root" "$AIUR_CONTROL_CALLER_ROOT" || continue
|
|
2202
|
-
match_count=$((match_count + 1))
|
|
2203
|
-
match_node="$AIUR_RECORD_NODE"
|
|
2204
|
-
match_key="$AIUR_RECORD_INSTANCE_KEY"
|
|
2205
|
-
match_session="$AIUR_RECORD_SESSION"
|
|
2206
|
-
match_socket="$AIUR_RECORD_SOCKET"
|
|
2207
|
-
match_root="$root"
|
|
2208
|
-
match_source="$AIUR_RECORD_PROJECT_ROOT_SOURCE"
|
|
2209
|
-
done
|
|
2210
|
-
|
|
2211
|
-
[ "$match_count" -eq 1 ] || return 0
|
|
2212
|
-
|
|
2213
|
-
AIUR_RELEASE_NODE="$match_node"
|
|
2214
|
-
AIUR_INSTANCE_KEY="$match_key"
|
|
2215
|
-
AIUR_PROJECT_ROOT="$match_root"
|
|
2216
|
-
AIUR_PROJECT_ROOT_SOURCE="$match_source"
|
|
2217
|
-
AIUR_ADOPTED_TMUX_SESSION="$match_session"
|
|
2218
|
-
AIUR_ADOPTED_TMUX_SOCKET="$match_socket"
|
|
2219
|
-
export AIUR_RELEASE_NODE AIUR_INSTANCE_KEY AIUR_PROJECT_ROOT AIUR_PROJECT_ROOT_SOURCE \
|
|
2220
|
-
AIUR_ADOPTED_TMUX_SESSION AIUR_ADOPTED_TMUX_SOCKET
|
|
2221
|
-
RELEASE_NODE="$AIUR_RELEASE_NODE"
|
|
2222
|
-
export RELEASE_NODE
|
|
2223
|
-
AIUR_CONTROL_ADOPTED_RECORD=1
|
|
2224
|
-
}
|
|
2225
|
-
|
|
2226
|
-
print_global_config_control_hint() {
|
|
2227
|
-
[ -n "${AIUR_CONTROL_HINT_ROOTS:-}" ] || return 0
|
|
2228
|
-
[ "${AIUR_CONTROL_CALLER_ROOT_SOURCE:-}" = "cwd" ] || return 0
|
|
2229
|
-
[ "${AIUR_CONTROL_CURRENT_NODE_STATE:-}" = "down" ] || return 0
|
|
2230
|
-
|
|
2231
|
-
echo "aiur: global-config control identity is keyed by cwd ${AIUR_CONTROL_CALLER_ROOT:-${AIUR_PROJECT_ROOT:-unknown}}" >&2
|
|
2232
|
-
echo "aiur: run control commands from the launch directory, or from a subdirectory of that launch directory" >&2
|
|
2233
|
-
echo "aiur: live launch directory candidate(s):" >&2
|
|
2234
|
-
printf '%s' "$AIUR_CONTROL_HINT_ROOTS" | sed '/^$/d; s/^/ /' >&2
|
|
2235
|
-
}
|
|
2236
|
-
|
|
2237
|
-
control_rpc_timeout_seconds() {
|
|
2238
|
-
local seconds="${AIUR_CONTROL_RPC_TIMEOUT_SECONDS:-10}"
|
|
2239
|
-
case "$seconds" in
|
|
2240
|
-
'' | *[!0-9]* | 0) seconds=10 ;;
|
|
2241
|
-
esac
|
|
2242
|
-
printf '%s' "$seconds"
|
|
2243
|
-
}
|
|
2244
|
-
|
|
2245
|
-
print_not_running_message() {
|
|
2246
|
-
echo "error: aiur is not running. Start it with \`aiurdev run\` (or \`aiurdev --bg\`), then retry." >&2
|
|
2247
|
-
}
|
|
2248
|
-
|
|
2249
|
-
print_control_down_message() {
|
|
2250
|
-
local crash_marker
|
|
2251
|
-
crash_marker="$(aiur_crash_marker_path)"
|
|
2252
|
-
if [ -f "$crash_marker" ]; then
|
|
2253
|
-
echo "aiur: background daemon at ${RELEASE_NODE} is DOWN after an unexpected exit; agents may be orphaned" >&2
|
|
2254
|
-
sed 's/^/ /' "$crash_marker" >&2 2>/dev/null || true
|
|
2255
|
-
echo "aiur: run 'aiur stop' to reap any orphaned agents, then start aiur again" >&2
|
|
2256
|
-
else
|
|
2257
|
-
print_not_running_message
|
|
2258
|
-
print_global_config_control_hint
|
|
2259
|
-
fi
|
|
2260
|
-
}
|
|
2261
|
-
|
|
2262
|
-
kill_control_rpc_process() {
|
|
2263
|
-
local pid="$1" grouped="$2" p tree=()
|
|
2264
|
-
[ -n "$pid" ] || return 0
|
|
2265
|
-
|
|
2266
|
-
# Snapshot descendants before signalling the wrapper. A release launcher may
|
|
2267
|
-
# fork its rpc BEAM into another process group and then exit; group signalling
|
|
2268
|
-
# alone would orphan that child and lose the only relationship we can reap.
|
|
2269
|
-
while IFS= read -r p; do tree+=("$p"); done < <(agent_pid_tree "$pid")
|
|
2270
|
-
|
|
2271
|
-
if [ "$grouped" = "1" ]; then
|
|
2272
|
-
kill -TERM "-$pid" 2>/dev/null || kill -TERM "$pid" 2>/dev/null || true
|
|
2273
|
-
fi
|
|
2274
|
-
|
|
2275
|
-
for p in "${tree[@]}"; do kill -TERM "$p" 2>/dev/null || true; done
|
|
2276
|
-
sleep 0.2
|
|
2277
|
-
if [ "$grouped" = "1" ]; then
|
|
2278
|
-
kill -KILL "-$pid" 2>/dev/null || true
|
|
2279
|
-
fi
|
|
2280
|
-
for p in "${tree[@]}"; do kill -KILL "$p" 2>/dev/null || true; done
|
|
2281
|
-
}
|
|
2282
|
-
|
|
2283
|
-
# Signalling the watchdog subshell alone leaves its `sleep` child running and
|
|
2284
|
-
# orphaned: bash forks the subshell, and the sleep is a separate process the
|
|
2285
|
-
# subshell's death does not reap. Kill the descendants too so a fast reply
|
|
2286
|
-
# leaves nothing behind that outlives the command.
|
|
2287
|
-
# Deepest-first: a bash subshell that is TERMed while waiting on a foreground
|
|
2288
|
-
# child defers the signal until that child exits, so signalling the wrapper
|
|
2289
|
-
# before the sleep it is waiting on can leave the sleep running.
|
|
2290
|
-
cancel_timeout_watchdog() {
|
|
2291
|
-
local pid="$1" p tree=() i
|
|
2292
|
-
[ -n "$pid" ] || return 0
|
|
2293
|
-
while IFS= read -r p; do tree+=("$p"); done < <(agent_pid_tree "$pid")
|
|
2294
|
-
for ((i = ${#tree[@]} - 1; i >= 0; i--)); do kill -TERM "${tree[i]}" 2>/dev/null || true; done
|
|
2295
|
-
}
|
|
2296
|
-
|
|
2297
|
-
run_release_rpc_with_timeout() {
|
|
2298
|
-
local expression="$1" timeout output_file timeout_file pid watchdog_pid status grouped=0
|
|
2299
|
-
timeout="$(control_rpc_timeout_seconds)"
|
|
2300
|
-
output_file="$(mktemp "${TMPDIR:-/tmp}/aiur-control-rpc-output.XXXXXX")"
|
|
2301
|
-
timeout_file="$(mktemp "${TMPDIR:-/tmp}/aiur-control-rpc-timeout.XXXXXX")"
|
|
2302
|
-
rm -f "$timeout_file" 2>/dev/null || true
|
|
2303
|
-
|
|
2304
|
-
if command -v setsid >/dev/null 2>&1; then
|
|
2305
|
-
setsid "$release_bin" rpc "$expression" >"$output_file" 2>&1 &
|
|
2306
|
-
grouped=1
|
|
2307
|
-
else
|
|
2308
|
-
"$release_bin" rpc "$expression" >"$output_file" 2>&1 &
|
|
2309
|
-
fi
|
|
2310
|
-
pid=$!
|
|
2311
|
-
|
|
2312
|
-
# The watchdog must not hold the caller's stdout/stderr. A caller that captures
|
|
2313
|
-
# our output (command substitution, a pipe, a language runtime's subprocess
|
|
2314
|
-
# capture) reads until EOF, and EOF arrives only when every process holding the
|
|
2315
|
-
# write end exits - including a watchdog that is merely sleeping. Closing those
|
|
2316
|
-
# descriptors here is what keeps the timeout a ceiling instead of a floor.
|
|
2317
|
-
(
|
|
2318
|
-
sleep "$timeout"
|
|
2319
|
-
if kill -0 "$pid" 2>/dev/null; then
|
|
2320
|
-
: >"$timeout_file"
|
|
2321
|
-
kill_control_rpc_process "$pid" "$grouped"
|
|
2322
|
-
fi
|
|
2323
|
-
) >/dev/null 2>&1 &
|
|
2324
|
-
watchdog_pid=$!
|
|
2325
|
-
|
|
2326
|
-
if wait "$pid"; then
|
|
2327
|
-
status=0
|
|
2328
|
-
else
|
|
2329
|
-
status=$?
|
|
2330
|
-
fi
|
|
2331
|
-
if [ -f "$timeout_file" ]; then
|
|
2332
|
-
# The timeout watchdog owns descendant cleanup. Once it has marked the
|
|
2333
|
-
# timeout, let it finish its TERM/KILL sequence; cancelling it here can
|
|
2334
|
-
# strand descendants that ignored TERM after the root process exits.
|
|
2335
|
-
:
|
|
2336
|
-
else
|
|
2337
|
-
cancel_timeout_watchdog "$watchdog_pid"
|
|
2338
|
-
fi
|
|
2339
|
-
wait "$watchdog_pid" 2>/dev/null || true
|
|
2340
|
-
|
|
2341
|
-
AIUR_CONTROL_RPC_OUTPUT="$(cat "$output_file" 2>/dev/null || true)"
|
|
2342
|
-
if [ -f "$timeout_file" ]; then
|
|
2343
|
-
AIUR_CONTROL_RPC_TIMED_OUT=1
|
|
2344
|
-
status=124
|
|
2345
|
-
else
|
|
2346
|
-
AIUR_CONTROL_RPC_TIMED_OUT=0
|
|
2347
|
-
fi
|
|
2348
|
-
|
|
2349
|
-
rm -f "$output_file" "$timeout_file" 2>/dev/null || true
|
|
2350
|
-
return "$status"
|
|
2351
|
-
}
|
|
2352
|
-
|
|
2353
|
-
control_command_label() {
|
|
2354
|
-
printf '%s' "${AIUR_CONTROL_COMMAND:-control rpc}"
|
|
2355
|
-
}
|
|
2356
|
-
|
|
2357
|
-
control_attempt_sentence() {
|
|
2358
|
-
[ -n "${AIUR_CONTROL_ATTEMPT_CONTEXT:-}" ] || return 0
|
|
2359
|
-
printf ' Attempted %s against daemon endpoint %s.' "$AIUR_CONTROL_ATTEMPT_CONTEXT" "${RELEASE_NODE:-unknown}"
|
|
2360
|
-
}
|
|
2361
|
-
|
|
2362
|
-
# Every line a control RPC surfaces to the operator routes through one of these
|
|
2363
|
-
# two helpers, so `run_control_rpc` can prove it never returns non-zero while
|
|
2364
|
-
# saying nothing (#1684): a silent failure is indistinguishable from a healthy
|
|
2365
|
-
# idle fleet, and nobody reads exit codes interactively.
|
|
2366
|
-
control_rpc_say() {
|
|
2367
|
-
AIUR_CONTROL_RPC_DIAGNOSED=1
|
|
2368
|
-
printf '%s\n' "$1" >&2
|
|
2369
|
-
}
|
|
2370
|
-
|
|
2371
|
-
control_rpc_echo_output() {
|
|
2372
|
-
[ -n "$1" ] || return 0
|
|
2373
|
-
AIUR_CONTROL_RPC_DIAGNOSED=1
|
|
2374
|
-
printf '%s\n' "$1" >&2
|
|
2375
|
-
}
|
|
2376
|
-
|
|
2377
|
-
# RPC an expression into the running node. The control CLI prints a trailing
|
|
2378
|
-
# `__AIUR_CONTROL_EXIT__:<code>` marker we translate into the process exit code.
|
|
2379
|
-
#
|
|
2380
|
-
# Wrapper contract: a non-zero return ALWAYS carries at least one stderr line
|
|
2381
|
-
# naming the command that failed. EX_TEMPFAIL (75) is exempt — it is the dev
|
|
2382
|
-
# shim's internal rebuild-and-retry signal, never an operator-visible outcome.
|
|
2383
|
-
run_control_rpc() {
|
|
2384
|
-
local status=0
|
|
2385
|
-
|
|
2386
|
-
AIUR_CONTROL_RPC_DIAGNOSED=0
|
|
2387
|
-
run_control_rpc_dispatch "$@" || status=$?
|
|
2388
|
-
|
|
2389
|
-
if [ "$status" -ne 0 ] && [ "$status" -ne 75 ] && [ "${AIUR_CONTROL_RPC_DIAGNOSED:-0}" -ne 1 ]; then
|
|
2390
|
-
if [ "$status" -eq 124 ]; then
|
|
2391
|
-
control_rpc_say "aiur: $(control_command_label) to ${RELEASE_NODE:-the daemon} timed out after $(control_rpc_timeout_seconds)s; outcome is unknown.$(control_attempt_sentence) The daemon did not reply within the budget - commonly one blocked process inside it, not host load. 'aiur alerts' is answered by a different process and can confirm the daemon is alive."
|
|
2392
|
-
elif [ -n "${AIUR_CONTROL_ATTEMPT_CONTEXT:-}" ]; then
|
|
2393
|
-
control_rpc_say "aiur: $(control_command_label) failed (exit ${status}) and produced no diagnostic output.$(control_attempt_sentence) Outcome is unknown. 'aiur alerts' is answered by a different process and can confirm the daemon is alive."
|
|
2394
|
-
else
|
|
2395
|
-
control_rpc_say "aiur: $(control_command_label) failed (exit ${status}) and produced no diagnostic output; outcome is unknown. 'aiur alerts' is answered by a different process and can confirm the daemon is alive."
|
|
2396
|
-
fi
|
|
2397
|
-
fi
|
|
2398
|
-
|
|
2399
|
-
return "$status"
|
|
2400
|
-
}
|
|
2401
|
-
|
|
2402
|
-
run_control_rpc_dispatch() {
|
|
2403
|
-
local expression="$1"
|
|
2404
|
-
resolve_release || return $?
|
|
2405
|
-
prepare_distribution || die "distribution setup failed; cannot contact aiur"
|
|
2406
|
-
resolve_control_identity_from_records
|
|
2407
|
-
if [ "${AIUR_CONTROL_ADOPTED_RECORD:-0}" -eq 1 ]; then
|
|
2408
|
-
prepare_distribution || die "distribution setup failed; cannot contact aiur"
|
|
2409
|
-
fi
|
|
2410
|
-
|
|
2411
|
-
local marker="__AIUR_CONTROL_EXIT__:" error_marker="__AIUR_CONTROL_ERROR__:"
|
|
2412
|
-
local output status exit_code=0 saw_marker=0 saw_error=0 saw_output=0 line partial_suffix=""
|
|
2413
|
-
|
|
2414
|
-
set +e
|
|
2415
|
-
run_release_rpc_with_timeout "$expression"
|
|
2416
|
-
status=$?
|
|
2417
|
-
output="$AIUR_CONTROL_RPC_OUTPUT"
|
|
2418
|
-
set -e
|
|
2419
|
-
|
|
2420
|
-
if [ "$status" -ne 0 ] && [ "${AIUR_CONTROL_RELEASE_RETRYABLE:-0}" = "1" ] && \
|
|
2421
|
-
{ [ ! -x "$release_bin" ] || [ ! -x "$vsn_dir/elixir" ] || [ ! -r "$release_dir/releases/start_erl.data" ]; }; then
|
|
2422
|
-
control_release_retry
|
|
2423
|
-
return $?
|
|
2424
|
-
fi
|
|
2425
|
-
|
|
2426
|
-
if [ "${AIUR_CONTROL_RPC_TIMED_OUT:-0}" -eq 1 ]; then
|
|
2427
|
-
[ -n "$output" ] && partial_suffix="; partial output was discarded"
|
|
2428
|
-
control_rpc_say "aiur: $(control_command_label) to ${RELEASE_NODE} timed out after $(control_rpc_timeout_seconds)s; outcome is unknown${partial_suffix}.$(control_attempt_sentence) Commonly one blocked process inside the daemon, not host load."
|
|
2429
|
-
return 124
|
|
2430
|
-
fi
|
|
2431
|
-
|
|
2432
|
-
if [ "$status" -ne 0 ]; then
|
|
2433
|
-
# A non-zero exit here is the rpc transport failing — application-level
|
|
2434
|
-
# outcomes ride the marker path below. `bin/aiur rpc` (Elixir --rpc-eval)
|
|
2435
|
-
# reports a genuinely-down node and a live-but-unreachable one identically
|
|
2436
|
-
# (`:noconnection`), and prints any exception raised inside the expression.
|
|
2437
|
-
# So the reason string can't be trusted; classify via epmd instead. Only a
|
|
2438
|
-
# node epmd confirms is down earns the friendly "start aiur" hint; an `up`
|
|
2439
|
-
# node failed for a real reason, and an `unknown` probe must not be assumed
|
|
2440
|
-
# down — in both of those cases surface the actual rpc output, never mask it.
|
|
2441
|
-
#
|
|
2442
|
-
# An empty buffer is its own case: `--rpc-eval` kills itself without a word
|
|
2443
|
-
# when the evaluated expression exits (a GenServer call timing out against a
|
|
2444
|
-
# saturated daemon does exactly that), so "see the error above" would point
|
|
2445
|
-
# at nothing (#1684). Name the failure instead.
|
|
2446
|
-
case "$(probe_node_liveness)" in
|
|
2447
|
-
down)
|
|
2448
|
-
print_control_down_message
|
|
2449
|
-
AIUR_CONTROL_RPC_DIAGNOSED=1
|
|
2450
|
-
;;
|
|
2451
|
-
up)
|
|
2452
|
-
control_rpc_echo_output "$output"
|
|
2453
|
-
if [ -n "$output" ]; then
|
|
2454
|
-
control_rpc_say "aiur: $(control_command_label) failed against ${RELEASE_NODE} (node is running); see the error above"
|
|
2455
|
-
elif [ -n "${AIUR_CONTROL_ATTEMPT_CONTEXT:-}" ]; then
|
|
2456
|
-
control_rpc_say "aiur: $(control_command_label) failed against ${RELEASE_NODE} with no diagnostic output (node is running).$(control_attempt_sentence) Outcome is unknown. 'aiur alerts' is answered by a different process and can confirm the daemon is alive."
|
|
2457
|
-
else
|
|
2458
|
-
control_rpc_say "aiur: $(control_command_label) failed against ${RELEASE_NODE} with no diagnostic output (node is running); outcome is unknown. 'aiur alerts' is answered by a different process and can confirm the daemon is alive."
|
|
2459
|
-
fi
|
|
2460
|
-
;;
|
|
2461
|
-
*)
|
|
2462
|
-
control_rpc_echo_output "$output"
|
|
2463
|
-
if [ -n "$output" ]; then
|
|
2464
|
-
control_rpc_say "aiur: $(control_command_label) failed against ${RELEASE_NODE} (could not query epmd to confirm node state); see the error above"
|
|
2465
|
-
elif [ -n "${AIUR_CONTROL_ATTEMPT_CONTEXT:-}" ]; then
|
|
2466
|
-
control_rpc_say "aiur: $(control_command_label) failed against ${RELEASE_NODE} with no output (could not query epmd to confirm node state).$(control_attempt_sentence) Outcome is unknown."
|
|
2467
|
-
else
|
|
2468
|
-
control_rpc_say "aiur: $(control_command_label) failed against ${RELEASE_NODE} with no output (could not query epmd to confirm node state)"
|
|
2469
|
-
fi
|
|
2470
|
-
;;
|
|
2471
|
-
esac
|
|
2472
|
-
return 1
|
|
2473
|
-
fi
|
|
2474
|
-
|
|
2475
|
-
while IFS= read -r line; do
|
|
2476
|
-
case "$line" in
|
|
2477
|
-
"$marker"*)
|
|
2478
|
-
saw_marker=1
|
|
2479
|
-
exit_code="${line#"$marker"}"
|
|
2480
|
-
;;
|
|
2481
|
-
"$error_marker"*)
|
|
2482
|
-
saw_error=1
|
|
2483
|
-
AIUR_CONTROL_RPC_DIAGNOSED=1
|
|
2484
|
-
printf '%s\n' "${line#"$error_marker"}" >&2
|
|
2485
|
-
;;
|
|
2486
|
-
:ok | "") ;;
|
|
2487
|
-
*)
|
|
2488
|
-
saw_output=1
|
|
2489
|
-
AIUR_CONTROL_RPC_DIAGNOSED=1
|
|
2490
|
-
printf '%s\n' "$line"
|
|
2491
|
-
;;
|
|
2492
|
-
esac
|
|
2493
|
-
done <<<"$output"
|
|
2494
|
-
|
|
2495
|
-
if [ "$saw_marker" -ne 1 ]; then
|
|
2496
|
-
if [ -n "${AIUR_CONTROL_ATTEMPT_CONTEXT:-}" ]; then
|
|
2497
|
-
control_rpc_say "aiur: $(control_command_label) to ${RELEASE_NODE} returned no exit marker; command output may be incomplete.$(control_attempt_sentence)"
|
|
2498
|
-
else
|
|
2499
|
-
control_rpc_say "aiur: $(control_command_label) to ${RELEASE_NODE} returned no exit marker; command output may be incomplete"
|
|
2500
|
-
fi
|
|
2501
|
-
return 1
|
|
2502
|
-
fi
|
|
2503
|
-
|
|
2504
|
-
if [ "$exit_code" -ne 0 ] && [ "$saw_error" -ne 1 ] && [ "$saw_output" -ne 1 ]; then
|
|
2505
|
-
if [ -n "${AIUR_CONTROL_ATTEMPT_CONTEXT:-}" ]; then
|
|
2506
|
-
control_rpc_say "aiur: $(control_command_label) failed with exit ${exit_code} and produced no diagnostic output.$(control_attempt_sentence) Outcome is unknown."
|
|
2507
|
-
else
|
|
2508
|
-
control_rpc_say "aiur: $(control_command_label) failed with exit ${exit_code} and returned no diagnostic output"
|
|
2509
|
-
fi
|
|
2510
|
-
fi
|
|
2511
|
-
|
|
2512
|
-
return "$exit_code"
|
|
2513
|
-
}
|
|
2514
|
-
|
|
2515
|
-
# A streaming Executor listener must retain the RPC process and its stdout. It
|
|
2516
|
-
# intentionally bypasses the one-shot control timeout and exit-marker wrapper.
|
|
2517
|
-
run_control_stream() {
|
|
2518
|
-
local expression="$1"
|
|
2519
|
-
resolve_release || return $?
|
|
2520
|
-
prepare_distribution || die "distribution setup failed; cannot contact aiur"
|
|
2521
|
-
resolve_control_identity_from_records
|
|
2522
|
-
if [ "${AIUR_CONTROL_ADOPTED_RECORD:-0}" -eq 1 ]; then
|
|
2523
|
-
prepare_distribution || die "distribution setup failed; cannot contact aiur"
|
|
2524
|
-
fi
|
|
2525
|
-
if [ "$(probe_node_liveness)" = "down" ]; then
|
|
2526
|
-
print_control_down_message
|
|
2527
|
-
return 1
|
|
2528
|
-
fi
|
|
2529
|
-
if "$release_bin" rpc "$expression"; then
|
|
2530
|
-
return 0
|
|
2531
|
-
else
|
|
2532
|
-
local status=$?
|
|
2533
|
-
echo "aiur: streaming control RPC failed with exit ${status}" >&2
|
|
2534
|
-
return "$status"
|
|
2535
|
-
fi
|
|
2536
|
-
}
|
|
2537
|
-
|
|
2538
|
-
elixir_list_literal() {
|
|
2539
|
-
local first=1 item
|
|
2540
|
-
printf '['
|
|
2541
|
-
for item in "$@"; do
|
|
2542
|
-
[ "$first" -eq 0 ] && printf ', '
|
|
2543
|
-
printf '"%s"' "$item"
|
|
2544
|
-
first=0
|
|
2545
|
-
done
|
|
2546
|
-
printf ']'
|
|
2547
|
-
}
|
|
2548
|
-
|
|
2549
|
-
# Parse issue-id targets (e.g. `44 45,46` or `--all`) into parsed_targets/parsed_all.
|
|
2550
|
-
parsed_targets=()
|
|
2551
|
-
parsed_all=0
|
|
2552
|
-
parse_issue_targets() {
|
|
2553
|
-
parsed_targets=()
|
|
2554
|
-
parsed_all=0
|
|
2555
|
-
[ "$#" -gt 0 ] || return 1
|
|
2556
|
-
|
|
2557
|
-
if [ "$#" -eq 1 ] && [ "$1" = "--all" ]; then
|
|
2558
|
-
parsed_all=1
|
|
2559
|
-
return 0
|
|
2560
|
-
fi
|
|
2561
|
-
|
|
2562
|
-
local raw part parts
|
|
2563
|
-
for raw in "$@"; do
|
|
2564
|
-
[ "$raw" = "--all" ] && return 1
|
|
2565
|
-
IFS=',' read -ra parts <<<"$raw"
|
|
2566
|
-
for part in "${parts[@]}"; do
|
|
2567
|
-
part="$(trim "$part")"
|
|
2568
|
-
if [ -z "$part" ] || [[ ! "$part" =~ ^[0-9]+$ ]]; then return 1; fi
|
|
2569
|
-
parsed_targets+=("$part")
|
|
2570
|
-
done
|
|
2571
|
-
done
|
|
2572
|
-
|
|
2573
|
-
[ "${#parsed_targets[@]}" -gt 0 ]
|
|
2574
|
-
}
|
|
2575
|
-
|
|
2576
|
-
cmd_status() {
|
|
2577
|
-
[ "$#" -eq 0 ] || die "status does not accept arguments"
|
|
2578
|
-
run_control_rpc "Aiur.AgentControlCLI.status()"
|
|
2579
|
-
}
|
|
2580
|
-
|
|
2581
|
-
# `aiur usage` — Codex/Claude limit headroom from the daemon's meter
|
|
2582
|
-
# projection, each value carrying the age of its observation.
|
|
2583
|
-
cmd_usage() {
|
|
2584
|
-
[ "$#" -eq 0 ] || die "usage does not accept arguments"
|
|
2585
|
-
run_control_rpc "Aiur.AgentControlCLI.usage()"
|
|
2586
|
-
}
|
|
2587
|
-
|
|
2588
|
-
cmd_pause_resume() {
|
|
2589
|
-
local command="$1"
|
|
2590
|
-
shift
|
|
2591
|
-
|
|
2592
|
-
# Bare `aiur pause` / `aiur resume` (no IDs, no --all) flips the single
|
|
2593
|
-
# global pause switch: a daemon-wide halt distinct from per-agent pause.
|
|
2594
|
-
if [ "$#" -eq 0 ]; then
|
|
2595
|
-
run_control_rpc "Aiur.AgentControlCLI.${command}_global()"
|
|
2596
|
-
return
|
|
2597
|
-
fi
|
|
2598
|
-
|
|
2599
|
-
if ! parse_issue_targets "$@"; then
|
|
2600
|
-
echo "aiur: $command expects issue IDs or --all (e.g. aiur $command 44 45,46; aiur $command --all; or bare aiur $command for the global switch)" >&2
|
|
2601
|
-
exit 64
|
|
2602
|
-
fi
|
|
2603
|
-
|
|
2604
|
-
local expression
|
|
2605
|
-
if [ "$parsed_all" -eq 1 ]; then
|
|
2606
|
-
expression="Aiur.AgentControlCLI.${command}(:all)"
|
|
2607
|
-
else
|
|
2608
|
-
expression="Aiur.AgentControlCLI.${command}($(elixir_list_literal "${parsed_targets[@]}"))"
|
|
2609
|
-
fi
|
|
2610
|
-
|
|
2611
|
-
run_control_rpc "$expression"
|
|
2612
|
-
}
|
|
2613
|
-
|
|
2614
|
-
# `aiur reset-budget <id>...` — clear the lifetime dispatch latch for one or
|
|
2615
|
-
# more tickets (the supported exit from the #1453 latch; no JSON hand-editing).
|
|
2616
|
-
cmd_reset_budget() {
|
|
2617
|
-
if ! parse_issue_targets "$@"; then
|
|
2618
|
-
echo "aiur: reset-budget expects issue IDs (e.g. aiur reset-budget 44 45,46)" >&2
|
|
2619
|
-
exit 64
|
|
2620
|
-
fi
|
|
2621
|
-
|
|
2622
|
-
# --all is rejected (exit 64 with guidance) rather than silently no-opping:
|
|
2623
|
-
# clearing every ticket's latch at once is not a documented operation and
|
|
2624
|
-
# would mask which tickets are structurally stuck (#1453 review P2d).
|
|
2625
|
-
if [ "$parsed_all" -eq 1 ]; then
|
|
2626
|
-
echo "aiur: reset-budget does not accept --all; name ticket IDs explicitly (e.g. aiur reset-budget 44 45,46)" >&2
|
|
2627
|
-
exit 64
|
|
2628
|
-
fi
|
|
2629
|
-
|
|
2630
|
-
local expression
|
|
2631
|
-
expression="Aiur.AgentControlCLI.reset_budget($(elixir_list_literal "${parsed_targets[@]}"))"
|
|
2632
|
-
run_control_rpc "$expression"
|
|
2633
|
-
}
|
|
2634
|
-
|
|
2635
|
-
# `aiur message <issue> <text>` — deliver Executor text to one running agent.
|
|
2636
|
-
# The text is base64-encoded for the RPC hop so arbitrary content (quotes,
|
|
2637
|
-
# backslashes, `#{}`, newlines) survives without Elixir-string escaping.
|
|
2638
|
-
cmd_message() {
|
|
2639
|
-
local usage="aiur: message expects an issue ID and text (e.g. aiur message 44 \"ship it\" or aiur message 44 --message-id ID \"ship it\")"
|
|
2640
|
-
local message_id="" message_id_given=0
|
|
2641
|
-
|
|
2642
|
-
local issue="${1:-}"
|
|
2643
|
-
if [ -z "$issue" ] || [[ ! "$issue" =~ ^[0-9]+$ ]]; then
|
|
2644
|
-
echo "$usage" >&2
|
|
2645
|
-
exit 64
|
|
2646
|
-
fi
|
|
2647
|
-
shift
|
|
2648
|
-
|
|
2649
|
-
# `--message-id ID` names this send, so a retry after an unknown outcome
|
|
2650
|
-
# returns the first copy instead of queueing a second one (#2717).
|
|
2651
|
-
case "${1:-}" in
|
|
2652
|
-
--message-id)
|
|
2653
|
-
[ "$#" -gt 1 ] || { echo "aiur: message --message-id requires a value" >&2; exit 64; }
|
|
2654
|
-
message_id="$2"
|
|
2655
|
-
message_id_given=1
|
|
2656
|
-
shift 2
|
|
2657
|
-
;;
|
|
2658
|
-
--message-id=*)
|
|
2659
|
-
message_id="${1#--message-id=}"
|
|
2660
|
-
message_id_given=1
|
|
2661
|
-
shift
|
|
2662
|
-
;;
|
|
2663
|
-
esac
|
|
2664
|
-
if [ "$message_id_given" = 1 ] && [[ ! "$message_id" =~ ^[A-Za-z0-9._:-]{1,128}$ ]]; then
|
|
2665
|
-
echo "aiur: message --message-id must be 1-128 letters, digits, '.', '_', ':' or '-' (it cannot be empty)" >&2
|
|
2666
|
-
exit 64
|
|
2667
|
-
fi
|
|
2668
|
-
|
|
2669
|
-
local text="$*"
|
|
2670
|
-
if [ -z "$text" ]; then
|
|
2671
|
-
echo "$usage" >&2
|
|
2672
|
-
exit 64
|
|
2673
|
-
fi
|
|
2674
|
-
|
|
2675
|
-
local encoded
|
|
2676
|
-
encoded="$(printf '%s' "$text" | base64 | tr -d '\n')"
|
|
2677
|
-
if [ -n "$message_id" ]; then
|
|
2678
|
-
run_control_rpc "Aiur.AgentControlCLI.message(\"$issue\", Base.decode64!(\"$encoded\"), \"$message_id\")"
|
|
2679
|
-
else
|
|
2680
|
-
run_control_rpc "Aiur.AgentControlCLI.message(\"$issue\", Base.decode64!(\"$encoded\"))"
|
|
2681
|
-
fi
|
|
2682
|
-
}
|
|
2683
|
-
|
|
2684
|
-
# `aiur agents` — concise one-line-per-agent state + current activity from a
|
|
2685
|
-
# live node (the headless equivalent of the dashboard / aiur-status skill).
|
|
2686
|
-
cmd_agents() {
|
|
2687
|
-
[ "$#" -eq 0 ] || die "agents does not accept arguments"
|
|
2688
|
-
run_control_rpc "Aiur.AgentControlCLI.agents()"
|
|
2689
|
-
}
|
|
2690
|
-
|
|
2691
|
-
# `aiur commands` — read the dashboard's retained Decision projection without
|
|
2692
|
-
# exposing any dispatch or answer mutation path.
|
|
2693
|
-
cmd_commands() {
|
|
2694
|
-
local filter="all" blocking=0 json=0 decision_id="" ticket="" search="" cursor="" limit="" arg
|
|
2695
|
-
|
|
2696
|
-
while [ "$#" -gt 0 ]; do
|
|
2697
|
-
arg="$1"
|
|
2698
|
-
case "$arg" in
|
|
2699
|
-
--filter) [ "$#" -gt 1 ] || { echo "aiur: commands --filter requires a value" >&2; exit 64; }; shift; filter="$1" ;;
|
|
2700
|
-
--filter=*) filter="${arg#--filter=}" ;;
|
|
2701
|
-
--blocking) blocking=1 ;;
|
|
2702
|
-
--ticket) [ "$#" -gt 1 ] || { echo "aiur: commands --ticket requires a value" >&2; exit 64; }; shift; ticket="$1" ;;
|
|
2703
|
-
--ticket=*) ticket="${arg#--ticket=}" ;;
|
|
2704
|
-
--search) [ "$#" -gt 1 ] || { echo "aiur: commands --search requires a value" >&2; exit 64; }; shift; search="$1" ;;
|
|
2705
|
-
--search=*) search="${arg#--search=}" ;;
|
|
2706
|
-
--cursor) [ "$#" -gt 1 ] || { echo "aiur: commands --cursor requires a value" >&2; exit 64; }; shift; cursor="$1" ;;
|
|
2707
|
-
--cursor=*) cursor="${arg#--cursor=}" ;;
|
|
2708
|
-
--limit) [ "$#" -gt 1 ] || { echo "aiur: commands --limit requires a value" >&2; exit 64; }; shift; limit="$1" ;;
|
|
2709
|
-
--limit=*) limit="${arg#--limit=}" ;;
|
|
2710
|
-
--json) json=1 ;;
|
|
2711
|
-
-*) echo "aiur: commands received an unknown option: $arg" >&2; exit 64 ;;
|
|
2712
|
-
*)
|
|
2713
|
-
if [ -n "$decision_id" ]; then
|
|
2714
|
-
echo "aiur: commands accepts at most one decision ID" >&2
|
|
2715
|
-
exit 64
|
|
2716
|
-
fi
|
|
2717
|
-
decision_id="$arg"
|
|
2718
|
-
;;
|
|
2719
|
-
esac
|
|
2720
|
-
shift
|
|
2721
|
-
done
|
|
2722
|
-
|
|
2723
|
-
case "$filter" in all|open|blocking|resolved) ;; *) echo "aiur: commands --filter accepts all, open, blocking, or resolved" >&2; exit 64 ;; esac
|
|
2724
|
-
[ -z "$limit" ] || [[ "$limit" =~ ^[1-9][0-9]*$ ]] || { echo "aiur: commands --limit expects a positive integer" >&2; exit 64; }
|
|
2725
|
-
[ -z "$ticket" ] || [ "$filter" = "all" ] || { echo "aiur: commands --ticket requires --filter all" >&2; exit 64; }
|
|
2726
|
-
[ -z "$search" ] || [ "$filter" = "all" ] || { echo "aiur: commands --search requires --filter all" >&2; exit 64; }
|
|
2727
|
-
|
|
2728
|
-
local opts="filter: :$filter"
|
|
2729
|
-
[ "$blocking" -eq 1 ] && opts="$opts, blocking: true"
|
|
2730
|
-
[ "$json" -eq 1 ] && opts="$opts, json: true"
|
|
2731
|
-
[ -n "$limit" ] && opts="$opts, limit: $limit"
|
|
2732
|
-
local key raw encoded
|
|
2733
|
-
for key in decision_id ticket search cursor; do
|
|
2734
|
-
raw="${!key}"
|
|
2735
|
-
[ -n "$raw" ] || continue
|
|
2736
|
-
encoded="$(printf '%s' "$raw" | base64 | tr -d '\n')"
|
|
2737
|
-
opts="$opts, $key: Base.decode64!(\"$encoded\")"
|
|
2738
|
-
done
|
|
2739
|
-
|
|
2740
|
-
run_control_rpc "Aiur.AgentControlCLI.commands([$opts])"
|
|
2741
|
-
}
|
|
2742
|
-
|
|
2743
|
-
encode_control_value() {
|
|
2744
|
-
printf '%s' "$1" | base64 | tr -d '\n'
|
|
2745
|
-
}
|
|
2746
|
-
|
|
2747
|
-
# These are explicit trusted-Executor mutations, deliberately separate from
|
|
2748
|
-
# the read-only `commands` catalog. Revisions remain dashboard-owned.
|
|
2749
|
-
cmd_executor_answer() {
|
|
2750
|
-
local decision_id="${1:-}" expected_version="" option_id="" custom_response="" rationale="" idempotency_key="" executor_id="aiur-cli" supersede=0 arg
|
|
2751
|
-
if [ -z "$decision_id" ] || [[ "$decision_id" = -* ]]; then
|
|
2752
|
-
echo "aiur: executor-answer expects exactly one decision ID" >&2
|
|
2753
|
-
exit 64
|
|
2754
|
-
fi
|
|
2755
|
-
shift
|
|
2756
|
-
|
|
2757
|
-
while [ "$#" -gt 0 ]; do
|
|
2758
|
-
arg="$1"
|
|
2759
|
-
case "$arg" in
|
|
2760
|
-
--expected-version) [ "$#" -gt 1 ] || { echo "aiur: executor-answer --expected-version requires a value" >&2; exit 64; }; shift; expected_version="$1" ;;
|
|
2761
|
-
--expected-version=*) expected_version="${arg#--expected-version=}" ;;
|
|
2762
|
-
--option) [ "$#" -gt 1 ] || { echo "aiur: executor-answer --option requires a value" >&2; exit 64; }; shift; option_id="$1" ;;
|
|
2763
|
-
--option=*) option_id="${arg#--option=}" ;;
|
|
2764
|
-
--custom-response) [ "$#" -gt 1 ] || { echo "aiur: executor-answer --custom-response requires a value" >&2; exit 64; }; shift; custom_response="$1" ;;
|
|
2765
|
-
--custom-response=*) custom_response="${arg#--custom-response=}" ;;
|
|
2766
|
-
--rationale) [ "$#" -gt 1 ] || { echo "aiur: executor-answer --rationale requires a value" >&2; exit 64; }; shift; rationale="$1" ;;
|
|
2767
|
-
--rationale=*) rationale="${arg#--rationale=}" ;;
|
|
2768
|
-
--idempotency-key) [ "$#" -gt 1 ] || { echo "aiur: executor-answer --idempotency-key requires a value" >&2; exit 64; }; shift; idempotency_key="$1" ;;
|
|
2769
|
-
--idempotency-key=*) idempotency_key="${arg#--idempotency-key=}" ;;
|
|
2770
|
-
--executor-id) [ "$#" -gt 1 ] || { echo "aiur: executor-answer --executor-id requires a value" >&2; exit 64; }; shift; executor_id="$1" ;;
|
|
2771
|
-
--executor-id=*) executor_id="${arg#--executor-id=}" ;;
|
|
2772
|
-
--supersede) supersede=1 ;;
|
|
2773
|
-
-*) echo "aiur: executor-answer received an unknown option: $arg" >&2; exit 64 ;;
|
|
2774
|
-
*) echo "aiur: executor-answer expects exactly one decision ID" >&2; exit 64 ;;
|
|
2775
|
-
esac
|
|
2776
|
-
shift
|
|
2777
|
-
done
|
|
2778
|
-
|
|
2779
|
-
[[ "$expected_version" =~ ^[1-9][0-9]*$ ]] || { echo "aiur: executor-answer --expected-version expects a positive integer" >&2; exit 64; }
|
|
2780
|
-
if { [ -n "$option_id" ] && [ -n "$custom_response" ]; } || { [ -z "$option_id" ] && [ -z "$custom_response" ]; }; then
|
|
2781
|
-
echo "aiur: executor-answer requires exactly one of --option or --custom-response" >&2
|
|
2782
|
-
exit 64
|
|
2783
|
-
fi
|
|
2784
|
-
[ -n "$rationale" ] || { echo "aiur: executor-answer --rationale is required" >&2; exit 64; }
|
|
2785
|
-
[ -n "$idempotency_key" ] || { echo "aiur: executor-answer --idempotency-key is required" >&2; exit 64; }
|
|
2786
|
-
[ -n "$executor_id" ] || { echo "aiur: executor-answer --executor-id must not be empty" >&2; exit 64; }
|
|
2787
|
-
|
|
2788
|
-
local opts="decision_id: Base.decode64!(\"$(encode_control_value "$decision_id")\"), expected_version: $expected_version"
|
|
2789
|
-
if [ -n "$option_id" ]; then
|
|
2790
|
-
opts="$opts, option_id: Base.decode64!(\"$(encode_control_value "$option_id")\")"
|
|
2791
|
-
else
|
|
2792
|
-
opts="$opts, custom_response: Base.decode64!(\"$(encode_control_value "$custom_response")\")"
|
|
2793
|
-
fi
|
|
2794
|
-
opts="$opts, rationale: Base.decode64!(\"$(encode_control_value "$rationale")\")"
|
|
2795
|
-
opts="$opts, idempotency_key: Base.decode64!(\"$(encode_control_value "$idempotency_key")\")"
|
|
2796
|
-
opts="$opts, executor_id: Base.decode64!(\"$(encode_control_value "$executor_id")\")"
|
|
2797
|
-
if [ "$supersede" -eq 1 ]; then
|
|
2798
|
-
opts="$opts, supersede: true"
|
|
2799
|
-
fi
|
|
2800
|
-
local AIUR_CONTROL_ATTEMPT_CONTEXT="decision ID ${decision_id} with expected version ${expected_version}"
|
|
2801
|
-
run_control_rpc "Aiur.AgentControlCLI.executor_answer([$opts])"
|
|
2802
|
-
}
|
|
2803
|
-
|
|
2804
|
-
cmd_executor_escalate() {
|
|
2805
|
-
local decision_id="${1:-}" expected_version="" reason="" executor_id="aiur-cli" arg
|
|
2806
|
-
if [ -z "$decision_id" ] || [[ "$decision_id" = -* ]]; then
|
|
2807
|
-
echo "aiur: executor-escalate expects exactly one decision ID" >&2
|
|
2808
|
-
exit 64
|
|
2809
|
-
fi
|
|
2810
|
-
shift
|
|
2811
|
-
|
|
2812
|
-
while [ "$#" -gt 0 ]; do
|
|
2813
|
-
arg="$1"
|
|
2814
|
-
case "$arg" in
|
|
2815
|
-
--expected-version) [ "$#" -gt 1 ] || { echo "aiur: executor-escalate --expected-version requires a value" >&2; exit 64; }; shift; expected_version="$1" ;;
|
|
2816
|
-
--expected-version=*) expected_version="${arg#--expected-version=}" ;;
|
|
2817
|
-
--reason) [ "$#" -gt 1 ] || { echo "aiur: executor-escalate --reason requires a value" >&2; exit 64; }; shift; reason="$1" ;;
|
|
2818
|
-
--reason=*) reason="${arg#--reason=}" ;;
|
|
2819
|
-
--executor-id) [ "$#" -gt 1 ] || { echo "aiur: executor-escalate --executor-id requires a value" >&2; exit 64; }; shift; executor_id="$1" ;;
|
|
2820
|
-
--executor-id=*) executor_id="${arg#--executor-id=}" ;;
|
|
2821
|
-
-*) echo "aiur: executor-escalate received an unknown option: $arg" >&2; exit 64 ;;
|
|
2822
|
-
*) echo "aiur: executor-escalate expects exactly one decision ID" >&2; exit 64 ;;
|
|
2823
|
-
esac
|
|
2824
|
-
shift
|
|
2825
|
-
done
|
|
2826
|
-
|
|
2827
|
-
[[ "$expected_version" =~ ^[1-9][0-9]*$ ]] || { echo "aiur: executor-escalate --expected-version expects a positive integer" >&2; exit 64; }
|
|
2828
|
-
[ -n "$reason" ] || { echo "aiur: executor-escalate --reason is required" >&2; exit 64; }
|
|
2829
|
-
[ -n "$executor_id" ] || { echo "aiur: executor-escalate --executor-id must not be empty" >&2; exit 64; }
|
|
2830
|
-
|
|
2831
|
-
local opts="decision_id: Base.decode64!(\"$(encode_control_value "$decision_id")\"), expected_version: $expected_version"
|
|
2832
|
-
opts="$opts, reason: Base.decode64!(\"$(encode_control_value "$reason")\")"
|
|
2833
|
-
opts="$opts, executor_id: Base.decode64!(\"$(encode_control_value "$executor_id")\")"
|
|
2834
|
-
local AIUR_CONTROL_ATTEMPT_CONTEXT="decision ID ${decision_id} with expected version ${expected_version}"
|
|
2835
|
-
run_control_rpc "Aiur.AgentControlCLI.executor_escalate([$opts])"
|
|
2836
|
-
}
|
|
2837
|
-
|
|
2838
|
-
cmd_executor_moot() {
|
|
2839
|
-
local decision_id="${1:-}" expected_version="" reason_class="" reason="" executor_id="aiur-cli" arg
|
|
2840
|
-
if [ -z "$decision_id" ] || [[ "$decision_id" = -* ]]; then
|
|
2841
|
-
echo "aiur: executor-moot expects exactly one decision ID" >&2
|
|
2842
|
-
exit 64
|
|
2843
|
-
fi
|
|
2844
|
-
shift
|
|
2845
|
-
|
|
2846
|
-
while [ "$#" -gt 0 ]; do
|
|
2847
|
-
arg="$1"
|
|
2848
|
-
case "$arg" in
|
|
2849
|
-
--expected-version) [ "$#" -gt 1 ] || { echo "aiur: executor-moot --expected-version requires a value" >&2; exit 64; }; shift; expected_version="$1" ;;
|
|
2850
|
-
--expected-version=*) expected_version="${arg#--expected-version=}" ;;
|
|
2851
|
-
--reason-class) [ "$#" -gt 1 ] || { echo "aiur: executor-moot --reason-class requires a value" >&2; exit 64; }; shift; reason_class="$1" ;;
|
|
2852
|
-
--reason-class=*) reason_class="${arg#--reason-class=}" ;;
|
|
2853
|
-
--reason) [ "$#" -gt 1 ] || { echo "aiur: executor-moot --reason requires a value" >&2; exit 64; }; shift; reason="$1" ;;
|
|
2854
|
-
--reason=*) reason="${arg#--reason=}" ;;
|
|
2855
|
-
--executor-id) [ "$#" -gt 1 ] || { echo "aiur: executor-moot --executor-id requires a value" >&2; exit 64; }; shift; executor_id="$1" ;;
|
|
2856
|
-
--executor-id=*) executor_id="${arg#--executor-id=}" ;;
|
|
2857
|
-
-*) echo "aiur: executor-moot received an unknown option: $arg" >&2; exit 64 ;;
|
|
2858
|
-
*) echo "aiur: executor-moot expects exactly one decision ID" >&2; exit 64 ;;
|
|
2859
|
-
esac
|
|
2860
|
-
shift
|
|
2861
|
-
done
|
|
2862
|
-
|
|
2863
|
-
[[ "$expected_version" =~ ^[1-9][0-9]*$ ]] || { echo "aiur: executor-moot --expected-version expects a positive integer" >&2; exit 64; }
|
|
2864
|
-
[ -n "$reason_class" ] || { echo "aiur: executor-moot --reason-class is required" >&2; exit 64; }
|
|
2865
|
-
[ -n "$executor_id" ] || { echo "aiur: executor-moot --executor-id must not be empty" >&2; exit 64; }
|
|
2866
|
-
|
|
2867
|
-
local opts="decision_id: Base.decode64!(\"$(encode_control_value "$decision_id")\"), expected_version: $expected_version"
|
|
2868
|
-
opts="$opts, reason_class: Base.decode64!(\"$(encode_control_value "$reason_class")\")"
|
|
2869
|
-
if [ -n "$reason" ]; then
|
|
2870
|
-
opts="$opts, reason: Base.decode64!(\"$(encode_control_value "$reason")\")"
|
|
2871
|
-
fi
|
|
2872
|
-
opts="$opts, executor_id: Base.decode64!(\"$(encode_control_value "$executor_id")\")"
|
|
2873
|
-
local AIUR_CONTROL_ATTEMPT_CONTEXT="decision ID ${decision_id} with expected version ${expected_version}"
|
|
2874
|
-
run_control_rpc "Aiur.AgentControlCLI.executor_moot([$opts])"
|
|
2875
|
-
}
|
|
2876
|
-
|
|
2877
|
-
# `aiur units` — read the dashboard Units catalog through its own projection,
|
|
2878
|
-
# including non-running tickets in current-run membership.
|
|
2879
|
-
cmd_units() {
|
|
2880
|
-
local scope="live" format="" json=0 arg condition condition_value condition_encoded conditions_literal=""
|
|
2881
|
-
local -a conditions=() condition_values=()
|
|
2882
|
-
while [ "$#" -gt 0 ]; do
|
|
2883
|
-
arg="$1"
|
|
2884
|
-
case "$arg" in
|
|
2885
|
-
--scope) [ "$#" -gt 1 ] || { echo "aiur: units --scope requires a value" >&2; exit 64; }; shift; scope="$1" ;;
|
|
2886
|
-
--scope=*) scope="${arg#--scope=}" ;;
|
|
2887
|
-
--condition) [ "$#" -gt 1 ] || { echo "aiur: units --condition requires a value" >&2; exit 64; }; shift; conditions+=("$1") ;;
|
|
2888
|
-
--condition=*) conditions+=("${arg#--condition=}") ;;
|
|
2889
|
-
--format) [ "$#" -gt 1 ] || { echo "aiur: units --format requires a value" >&2; exit 64; }; shift; format="$1" ;;
|
|
2890
|
-
--format=*) format="${arg#--format=}" ;;
|
|
2891
|
-
--json) json=1 ;;
|
|
2892
|
-
-*) echo "aiur: units received an unknown option: $arg" >&2; exit 64 ;;
|
|
2893
|
-
*) echo "aiur: units does not accept positional arguments" >&2; exit 64 ;;
|
|
2894
|
-
esac
|
|
2895
|
-
shift
|
|
2896
|
-
done
|
|
2897
|
-
|
|
2898
|
-
for condition in "${conditions[@]+"${conditions[@]}"}"; do
|
|
2899
|
-
IFS=',' read -r -a condition_values <<< "$condition"
|
|
2900
|
-
for condition_value in "${condition_values[@]+"${condition_values[@]}"}"; do
|
|
2901
|
-
if [ -n "$conditions_literal" ]; then conditions_literal="$conditions_literal, "; fi
|
|
2902
|
-
condition_encoded="$(printf '%s' "$condition_value" | base64 | tr -d '\n')"
|
|
2903
|
-
conditions_literal="${conditions_literal}Base.decode64!(\"${condition_encoded}\")"
|
|
2904
|
-
done
|
|
2905
|
-
done
|
|
2906
|
-
|
|
2907
|
-
local scope_encoded
|
|
2908
|
-
scope_encoded="$(printf '%s' "$scope" | base64 | tr -d '\n')"
|
|
2909
|
-
local opts="scope: Base.decode64!(\"$scope_encoded\")"
|
|
2910
|
-
[ -z "$conditions_literal" ] || opts="$opts, conditions: [$conditions_literal]"
|
|
2911
|
-
|
|
2912
|
-
if [ -n "$format" ]; then
|
|
2913
|
-
local format_encoded
|
|
2914
|
-
format_encoded="$(printf '%s' "$format" | base64 | tr -d '\n')"
|
|
2915
|
-
opts="$opts, format: Base.decode64!(\"$format_encoded\")"
|
|
2916
|
-
fi
|
|
2917
|
-
|
|
2918
|
-
[ "$json" -eq 1 ] && opts="$opts, json: true"
|
|
2919
|
-
run_control_rpc "Aiur.AgentControlCLI.units([$opts])"
|
|
2920
|
-
}
|
|
2921
|
-
|
|
2922
|
-
# `aiur build-orders` — read the dashboard Build Order projection without a
|
|
2923
|
-
# second GitHub or API derivation. A root selector switches from catalog to the
|
|
2924
|
-
# selected-root graph view.
|
|
2925
|
-
cmd_build_orders() {
|
|
2926
|
-
local json=0 root="" arg
|
|
2927
|
-
|
|
2928
|
-
while [ "$#" -gt 0 ]; do
|
|
2929
|
-
arg="$1"
|
|
2930
|
-
case "$arg" in
|
|
2931
|
-
--json) json=1 ;;
|
|
2932
|
-
-*) echo "aiur: build-orders received an unknown option: $arg" >&2; exit 64 ;;
|
|
2933
|
-
*)
|
|
2934
|
-
if [ -n "$root" ]; then
|
|
2935
|
-
echo "aiur: build-orders accepts at most one root" >&2
|
|
2936
|
-
exit 64
|
|
2937
|
-
fi
|
|
2938
|
-
root="$arg"
|
|
2939
|
-
;;
|
|
2940
|
-
esac
|
|
2941
|
-
shift
|
|
2942
|
-
done
|
|
2943
|
-
|
|
2944
|
-
local opts=""
|
|
2945
|
-
[ "$json" -eq 1 ] && opts="json: true"
|
|
2946
|
-
|
|
2947
|
-
if [ -n "$root" ]; then
|
|
2948
|
-
local encoded
|
|
2949
|
-
encoded="$(printf '%s' "$root" | base64 | tr -d '\n')"
|
|
2950
|
-
[ -n "$opts" ] && opts="$opts, "
|
|
2951
|
-
opts="${opts}root: Base.decode64!(\"$encoded\")"
|
|
2952
|
-
fi
|
|
2953
|
-
|
|
2954
|
-
run_control_rpc "Aiur.AgentControlCLI.build_orders([$opts])"
|
|
2955
|
-
}
|
|
2956
|
-
|
|
2957
|
-
# `aiur analytics` — render the dashboard analytics projection for an explicit
|
|
2958
|
-
# time window. This is read-only and obtains the same durable telemetry snapshot
|
|
2959
|
-
# the page uses through the running node.
|
|
2960
|
-
cmd_analytics() {
|
|
2961
|
-
local range="run" json=0 since="" until="" build_order="" has_build_order=0 arg
|
|
2962
|
-
|
|
2963
|
-
while [ "$#" -gt 0 ]; do
|
|
2964
|
-
arg="$1"
|
|
2965
|
-
case "$arg" in
|
|
2966
|
-
--range) [ "$#" -gt 1 ] || { echo "aiur: analytics --range requires a value" >&2; exit 64; }; shift; range="$1" ;;
|
|
2967
|
-
--range=*) range="${arg#--range=}" ;;
|
|
2968
|
-
--since) [ "$#" -gt 1 ] || { echo "aiur: analytics --since requires a value" >&2; exit 64; }; shift; since="$1" ;;
|
|
2969
|
-
--since=*) since="${arg#--since=}" ;;
|
|
2970
|
-
--until) [ "$#" -gt 1 ] || { echo "aiur: analytics --until requires a value" >&2; exit 64; }; shift; until="$1" ;;
|
|
2971
|
-
--until=*) until="${arg#--until=}" ;;
|
|
2972
|
-
--build-order) [ "$#" -gt 1 ] || { echo "aiur: analytics --build-order requires a value" >&2; exit 64; }; shift; build_order="$1"; has_build_order=1 ;;
|
|
2973
|
-
--build-order=*) build_order="${arg#--build-order=}"; has_build_order=1 ;;
|
|
2974
|
-
--json) json=1 ;;
|
|
2975
|
-
-*) echo "aiur: analytics received an unknown option: $arg" >&2; exit 64 ;;
|
|
2976
|
-
*) echo "aiur: analytics does not accept positional arguments" >&2; exit 64 ;;
|
|
2977
|
-
esac
|
|
2978
|
-
shift
|
|
2979
|
-
done
|
|
2980
|
-
|
|
2981
|
-
case "$range" in run|full) ;; *) echo "aiur: analytics --range accepts run or full" >&2; exit 64 ;; esac
|
|
2982
|
-
[ "$has_build_order" -eq 0 ] || [[ "$build_order" =~ ^[0-9]+$ ]] || { echo "aiur: analytics --build-order expects a numeric ticket ID" >&2; exit 64; }
|
|
2983
|
-
|
|
2984
|
-
local opts="range: :$range" key raw encoded
|
|
2985
|
-
[ "$json" -eq 1 ] && opts="$opts, json: true"
|
|
2986
|
-
for key in since until build_order; do
|
|
2987
|
-
raw="${!key}"
|
|
2988
|
-
[ -n "$raw" ] || continue
|
|
2989
|
-
encoded="$(printf '%s' "$raw" | base64 | tr -d '\n')"
|
|
2990
|
-
opts="$opts, $key: Base.decode64!(\"$encoded\")"
|
|
2991
|
-
done
|
|
2992
|
-
|
|
2993
|
-
run_control_rpc "Aiur.AgentControlCLI.analytics([$opts])"
|
|
2994
|
-
}
|
|
2995
|
-
|
|
2996
|
-
# `aiur github-cost` — GitHub API spend ranked by the call site that caused it,
|
|
2997
|
-
# in points per hour, with the reconciliation against the credential's own
|
|
2998
|
-
# `used` figure printed beside it. Read-only; it reads the meter the daemon
|
|
2999
|
-
# already keeps and issues no GitHub request of its own.
|
|
3000
|
-
cmd_github_cost() {
|
|
3001
|
-
local budget="graphql" format="" json=0 arg
|
|
3002
|
-
|
|
3003
|
-
while [ "$#" -gt 0 ]; do
|
|
3004
|
-
arg="$1"
|
|
3005
|
-
case "$arg" in
|
|
3006
|
-
--budget) [ "$#" -gt 1 ] || { echo "aiur: github-cost --budget requires a value" >&2; exit 64; }; shift; budget="$1" ;;
|
|
3007
|
-
--budget=*) budget="${arg#--budget=}" ;;
|
|
3008
|
-
--format) [ "$#" -gt 1 ] || { echo "aiur: github-cost --format requires a value" >&2; exit 64; }; shift; format="$1" ;;
|
|
3009
|
-
--format=*) format="${arg#--format=}" ;;
|
|
3010
|
-
--json) json=1 ;;
|
|
3011
|
-
-*) echo "aiur: github-cost received an unknown option: $arg" >&2; exit 64 ;;
|
|
3012
|
-
*) echo "aiur: github-cost does not accept positional arguments" >&2; exit 64 ;;
|
|
3013
|
-
esac
|
|
3014
|
-
shift
|
|
3015
|
-
done
|
|
3016
|
-
|
|
3017
|
-
case "$budget" in graphql|core|all) ;; *) echo "aiur: github-cost --budget accepts graphql, core or all" >&2; exit 64 ;; esac
|
|
3018
|
-
case "$format" in ""|auto|table|records) ;; *) echo "aiur: github-cost --format accepts auto, table or records" >&2; exit 64 ;; esac
|
|
3019
|
-
|
|
3020
|
-
# Both values are whitelisted above, so neither reaches the control RPC as
|
|
3021
|
-
# anything other than one of the literals named here.
|
|
3022
|
-
local opts="budget: \"$budget\""
|
|
3023
|
-
[ -z "$format" ] || opts="$opts, format: :$format"
|
|
3024
|
-
[ "$json" -eq 1 ] && opts="$opts, json: true"
|
|
3025
|
-
|
|
3026
|
-
run_control_rpc "Aiur.AgentControlCLI.github_cost([$opts])"
|
|
3027
|
-
}
|
|
3028
|
-
|
|
3029
|
-
# `aiur github-usage` — per-actor (daemon vs each agent workspace) Core/GraphQL
|
|
3030
|
-
# usage and ceilings from the shared admission broker. Read-only; it reads the
|
|
3031
|
-
# broker database and issues no GitHub request of its own.
|
|
3032
|
-
cmd_github_usage() {
|
|
3033
|
-
local json=0 arg
|
|
3034
|
-
|
|
3035
|
-
while [ "$#" -gt 0 ]; do
|
|
3036
|
-
arg="$1"
|
|
3037
|
-
case "$arg" in
|
|
3038
|
-
--json) json=1 ;;
|
|
3039
|
-
-*) echo "aiur: github-usage received an unknown option: $arg" >&2; exit 64 ;;
|
|
3040
|
-
*) echo "aiur: github-usage does not accept positional arguments" >&2; exit 64 ;;
|
|
3041
|
-
esac
|
|
3042
|
-
shift
|
|
3043
|
-
done
|
|
3044
|
-
|
|
3045
|
-
local opts=""
|
|
3046
|
-
[ "$json" -eq 1 ] && opts="json: true"
|
|
3047
|
-
|
|
3048
|
-
run_control_rpc "Aiur.AgentControlCLI.github_usage([$opts])"
|
|
3049
|
-
}
|
|
3050
|
-
|
|
3051
|
-
# `aiur alerts` — newline-delimited structured alert feed from persisted
|
|
3052
|
-
# per-agent logs. `--needs-attention` filters to Executor-actionable alerts.
|
|
3053
|
-
cmd_alerts() {
|
|
3054
|
-
local needs_attention=0 arg
|
|
3055
|
-
for arg in "$@"; do
|
|
3056
|
-
case "$arg" in
|
|
3057
|
-
--needs-attention) needs_attention=1 ;;
|
|
3058
|
-
*)
|
|
3059
|
-
echo "aiur: alerts only accepts --needs-attention" >&2
|
|
3060
|
-
exit 64
|
|
3061
|
-
;;
|
|
3062
|
-
esac
|
|
3063
|
-
done
|
|
3064
|
-
|
|
3065
|
-
if [ "$needs_attention" -eq 1 ]; then
|
|
3066
|
-
run_control_rpc "Aiur.AgentControlCLI.alerts(needs_attention: true)"
|
|
3067
|
-
else
|
|
3068
|
-
run_control_rpc "Aiur.AgentControlCLI.alerts()"
|
|
3069
|
-
fi
|
|
3070
|
-
}
|
|
3071
|
-
|
|
3072
|
-
# `aiur watch` — the server-side status board. Compiles one row per active
|
|
3073
|
-
# agent (state · complexity · activity-age · doing) plus an actionable section
|
|
3074
|
-
# from aiur's own state, with no GitHub round-trip. `--changes` (default) prints
|
|
3075
|
-
# only state-level deltas since the last call; `--full` prints every row;
|
|
3076
|
-
# `--interval N` re-renders every N seconds as a foreground watcher (the Elixir
|
|
3077
|
-
# call stays one-shot — the loop lives here).
|
|
3078
|
-
cmd_watch() {
|
|
3079
|
-
local mode="changes" interval="" arg
|
|
3080
|
-
|
|
3081
|
-
while [ "$#" -gt 0 ]; do
|
|
3082
|
-
arg="$1"
|
|
3083
|
-
case "$arg" in
|
|
3084
|
-
--full) mode="full" ;;
|
|
3085
|
-
--changes) mode="changes" ;;
|
|
3086
|
-
--once) : ;;
|
|
3087
|
-
--interval)
|
|
3088
|
-
shift
|
|
3089
|
-
interval="${1:-}"
|
|
3090
|
-
watch_validate_interval "$interval"
|
|
3091
|
-
;;
|
|
3092
|
-
--interval=*)
|
|
3093
|
-
interval="${arg#--interval=}"
|
|
3094
|
-
watch_validate_interval "$interval"
|
|
3095
|
-
;;
|
|
3096
|
-
*)
|
|
3097
|
-
echo "aiur: watch accepts --full, --changes, --once, --interval <secs>" >&2
|
|
3098
|
-
exit 64
|
|
3099
|
-
;;
|
|
3100
|
-
esac
|
|
3101
|
-
shift
|
|
3102
|
-
done
|
|
3103
|
-
|
|
3104
|
-
local expression="Aiur.AgentControlCLI.watch(mode: :${mode})"
|
|
3105
|
-
|
|
3106
|
-
if [ -n "$interval" ]; then
|
|
3107
|
-
while true; do
|
|
3108
|
-
run_control_rpc "$expression" || true
|
|
3109
|
-
sleep "$interval"
|
|
3110
|
-
done
|
|
3111
|
-
else
|
|
3112
|
-
run_control_rpc "$expression"
|
|
3113
|
-
fi
|
|
3114
|
-
}
|
|
3115
|
-
|
|
3116
|
-
cmd_executor_listen() {
|
|
3117
|
-
local topic="executor.#" arg
|
|
3118
|
-
while [ "$#" -gt 0 ]; do
|
|
3119
|
-
arg="$1"
|
|
3120
|
-
case "$arg" in
|
|
3121
|
-
--topic) shift; topic="${1:-}" ;;
|
|
3122
|
-
--topic=*) topic="${arg#--topic=}" ;;
|
|
3123
|
-
*) echo "aiur: executor-listen accepts --topic <pattern>" >&2; exit 64 ;;
|
|
3124
|
-
esac
|
|
3125
|
-
[ -n "$topic" ] || { echo "aiur: executor-listen requires a topic" >&2; exit 64; }
|
|
3126
|
-
shift
|
|
3127
|
-
done
|
|
3128
|
-
local encoded
|
|
3129
|
-
encoded="$(printf '%s' "$topic" | base64 | tr -d '\n')"
|
|
3130
|
-
run_control_stream "Aiur.AgentControlCLI.executor_listen(topic: Base.decode64!(\"$encoded\"))"
|
|
3131
|
-
}
|
|
3132
|
-
|
|
3133
|
-
cmd_executor_wait() {
|
|
3134
|
-
local timeout=300 json=0 as="" arg
|
|
3135
|
-
while [ "$#" -gt 0 ]; do
|
|
3136
|
-
arg="$1"
|
|
3137
|
-
case "$arg" in
|
|
3138
|
-
--timeout) shift; timeout="${1:-}" ;;
|
|
3139
|
-
--timeout=*) timeout="${arg#--timeout=}" ;;
|
|
3140
|
-
--json) json=1 ;;
|
|
3141
|
-
--as) shift; as="${1:-}" ;;
|
|
3142
|
-
--as=*) as="${arg#--as=}" ;;
|
|
3143
|
-
*) echo "aiur: executor-wait accepts --timeout <seconds>, --as <id> and --json" >&2; exit 64 ;;
|
|
3144
|
-
esac
|
|
3145
|
-
shift
|
|
3146
|
-
done
|
|
3147
|
-
[[ "$timeout" =~ ^[1-9][0-9]*$ ]] || { echo "aiur: executor-wait --timeout expects a positive integer" >&2; exit 64; }
|
|
3148
|
-
executor_validate_consumer_id "$as" "executor-wait"
|
|
3149
|
-
local json_arg=false
|
|
3150
|
-
if [ "$json" -eq 1 ]; then json_arg=true; fi
|
|
3151
|
-
AIUR_CONTROL_COMMAND="executor-wait"
|
|
3152
|
-
AIUR_CONTROL_RPC_TIMEOUT_SECONDS=$((timeout + 10)) run_control_rpc "Aiur.AgentControlCLI.executor_wait(timeout_ms: $((timeout * 1000)), json: $json_arg$(executor_as_argument "$as"))"
|
|
3153
|
-
}
|
|
3154
|
-
|
|
3155
|
-
# The consumer id is an explicit identity, never inferred from the environment.
|
|
3156
|
-
executor_validate_consumer_id() {
|
|
3157
|
-
local value="$1" command="$2"
|
|
3158
|
-
[ -n "$value" ] || return 0
|
|
3159
|
-
[[ "$value" =~ ^[A-Za-z0-9._-]+$ ]] || { echo "aiur: $command --as expects [A-Za-z0-9._-]+" >&2; exit 64; }
|
|
3160
|
-
}
|
|
3161
|
-
|
|
3162
|
-
executor_as_argument() {
|
|
3163
|
-
[ -n "$1" ] || return 0
|
|
3164
|
-
printf ', as: "%s"' "$1"
|
|
3165
|
-
}
|
|
3166
|
-
|
|
3167
|
-
executor_as_keyword() {
|
|
3168
|
-
[ -n "$1" ] || return 0
|
|
3169
|
-
printf 'as: "%s"' "$1"
|
|
3170
|
-
}
|
|
3171
|
-
|
|
3172
|
-
cmd_executor_roster() {
|
|
3173
|
-
local json=0 arg
|
|
3174
|
-
while [ "$#" -gt 0 ]; do
|
|
3175
|
-
arg="$1"
|
|
3176
|
-
case "$arg" in
|
|
3177
|
-
--json) json=1 ;;
|
|
3178
|
-
*) echo "aiur: executor-roster accepts only --json" >&2; exit 64 ;;
|
|
3179
|
-
esac
|
|
3180
|
-
shift
|
|
3181
|
-
done
|
|
3182
|
-
local json_arg=false
|
|
3183
|
-
if [ "$json" -eq 1 ]; then json_arg=true; fi
|
|
3184
|
-
run_control_rpc "Aiur.AgentControlCLI.executor_roster(json: $json_arg)"
|
|
3185
|
-
}
|
|
3186
|
-
|
|
3187
|
-
cmd_executor_fast_forward() {
|
|
3188
|
-
local wake_id="${1:-}" as="" arg
|
|
3189
|
-
[ -n "$wake_id" ] || { echo "aiur: executor-fast-forward expects a positive wake id" >&2; exit 64; }
|
|
3190
|
-
shift
|
|
3191
|
-
[[ "$wake_id" =~ ^[1-9][0-9]*$ ]] || { echo "aiur: executor-fast-forward expects a positive wake id" >&2; exit 64; }
|
|
3192
|
-
|
|
3193
|
-
while [ "$#" -gt 0 ]; do
|
|
3194
|
-
arg="$1"
|
|
3195
|
-
case "$arg" in
|
|
3196
|
-
--as)
|
|
3197
|
-
shift
|
|
3198
|
-
[ "$#" -gt 0 ] && [ -n "${1:-}" ] || { echo "aiur: executor-fast-forward --as requires a consumer id" >&2; exit 64; }
|
|
3199
|
-
as="$1"
|
|
3200
|
-
;;
|
|
3201
|
-
--as=*)
|
|
3202
|
-
as="${arg#--as=}"
|
|
3203
|
-
[ -n "$as" ] || { echo "aiur: executor-fast-forward --as requires a consumer id" >&2; exit 64; }
|
|
3204
|
-
;;
|
|
3205
|
-
*) echo "aiur: executor-fast-forward accepts <wake-id> and --as <id>" >&2; exit 64 ;;
|
|
3206
|
-
esac
|
|
3207
|
-
shift
|
|
3208
|
-
done
|
|
3209
|
-
|
|
3210
|
-
executor_validate_consumer_id "$as" "executor-fast-forward"
|
|
3211
|
-
run_control_rpc "Aiur.AgentControlCLI.executor_fast_forward($wake_id, [$(executor_as_keyword "$as")])"
|
|
3212
|
-
}
|
|
3213
|
-
|
|
3214
|
-
cmd_executor_claim() {
|
|
3215
|
-
local as="" arg
|
|
3216
|
-
while [ "$#" -gt 0 ]; do
|
|
3217
|
-
arg="$1"
|
|
3218
|
-
case "$arg" in
|
|
3219
|
-
--as) shift; as="${1:-}" ;;
|
|
3220
|
-
--as=*) as="${arg#--as=}" ;;
|
|
3221
|
-
*) echo "aiur: executor-claim accepts only --as <id>" >&2; exit 64 ;;
|
|
3222
|
-
esac
|
|
3223
|
-
shift
|
|
3224
|
-
done
|
|
3225
|
-
executor_validate_consumer_id "$as" "executor-claim"
|
|
3226
|
-
run_control_rpc "Aiur.AgentControlCLI.executor_claim([$(executor_as_keyword "$as")])"
|
|
3227
|
-
}
|
|
3228
|
-
|
|
3229
|
-
cmd_executor_release() {
|
|
3230
|
-
local as="" arg
|
|
3231
|
-
while [ "$#" -gt 0 ]; do
|
|
3232
|
-
arg="$1"
|
|
3233
|
-
case "$arg" in
|
|
3234
|
-
--as) shift; as="${1:-}" ;;
|
|
3235
|
-
--as=*) as="${arg#--as=}" ;;
|
|
3236
|
-
*) echo "aiur: executor-release accepts only --as <id>" >&2; exit 64 ;;
|
|
3237
|
-
esac
|
|
3238
|
-
shift
|
|
3239
|
-
done
|
|
3240
|
-
executor_validate_consumer_id "$as" "executor-release"
|
|
3241
|
-
run_control_rpc "Aiur.AgentControlCLI.executor_release([$(executor_as_keyword "$as")])"
|
|
3242
|
-
}
|
|
3243
|
-
|
|
3244
|
-
cmd_executor_revoke() {
|
|
3245
|
-
local owner="${1:-}"
|
|
3246
|
-
[ -n "$owner" ] || { echo "aiur: executor-revoke requires the current owner's consumer id" >&2; exit 64; }
|
|
3247
|
-
shift
|
|
3248
|
-
[ "$#" -eq 0 ] || { echo "aiur: executor-revoke accepts only <consumer-id>" >&2; exit 64; }
|
|
3249
|
-
executor_validate_consumer_id "$owner" "executor-revoke"
|
|
3250
|
-
run_control_rpc "Aiur.AgentControlCLI.executor_revoke(\"$owner\")"
|
|
3251
|
-
}
|
|
3252
|
-
|
|
3253
|
-
cmd_executor_emit() {
|
|
3254
|
-
local topic="${1:-}" payload="" arg
|
|
3255
|
-
shift 2>/dev/null || true
|
|
3256
|
-
[ -n "$topic" ] || { echo "aiur: executor-emit requires a topic" >&2; exit 64; }
|
|
3257
|
-
while [ "$#" -gt 0 ]; do
|
|
3258
|
-
arg="$1"
|
|
3259
|
-
case "$arg" in
|
|
3260
|
-
--payload) shift; payload="${1:-}" ;;
|
|
3261
|
-
--payload=*) payload="${arg#--payload=}" ;;
|
|
3262
|
-
*) echo "aiur: executor-emit accepts only --payload <json>" >&2; exit 64 ;;
|
|
3263
|
-
esac
|
|
3264
|
-
shift
|
|
3265
|
-
done
|
|
3266
|
-
[ -n "$payload" ] || { echo "aiur: executor-emit requires --payload <json>" >&2; exit 64; }
|
|
3267
|
-
local topic_encoded payload_encoded
|
|
3268
|
-
topic_encoded="$(printf '%s' "$topic" | base64 | tr -d '\n')"
|
|
3269
|
-
payload_encoded="$(printf '%s' "$payload" | base64 | tr -d '\n')"
|
|
3270
|
-
run_control_rpc "Aiur.AgentControlCLI.executor_emit(Base.decode64!(\"$topic_encoded\"), Base.decode64!(\"$payload_encoded\"))"
|
|
3271
|
-
}
|
|
3272
|
-
|
|
3273
|
-
cmd_executor_subscription() {
|
|
3274
|
-
local action="$1" topic="${2:-}"
|
|
3275
|
-
[ -n "$topic" ] && [ "$#" -eq 2 ] || { echo "aiur: $action requires one topic pattern" >&2; exit 64; }
|
|
3276
|
-
local encoded
|
|
3277
|
-
encoded="$(printf '%s' "$topic" | base64 | tr -d '\n')"
|
|
3278
|
-
case "$action" in
|
|
3279
|
-
executor-subscribe) run_control_rpc "Aiur.AgentControlCLI.executor_subscribe(Base.decode64!(\"$encoded\"))" ;;
|
|
3280
|
-
executor-unsubscribe) run_control_rpc "Aiur.AgentControlCLI.executor_unsubscribe(Base.decode64!(\"$encoded\"))" ;;
|
|
3281
|
-
esac
|
|
3282
|
-
}
|
|
3283
|
-
|
|
3284
|
-
cmd_executor_subscriptions() {
|
|
3285
|
-
[ "$#" -eq 0 ] || { echo "aiur: executor-subscriptions does not accept arguments" >&2; exit 64; }
|
|
3286
|
-
run_control_rpc "Aiur.AgentControlCLI.executor_subscriptions()"
|
|
3287
|
-
}
|
|
3288
|
-
|
|
3289
|
-
watch_validate_interval() {
|
|
3290
|
-
if ! [[ "$1" =~ ^[0-9]+$ ]] || [ "$1" -le 0 ]; then
|
|
3291
|
-
echo "aiur: watch --interval expects a positive integer (seconds)" >&2
|
|
3292
|
-
exit 64
|
|
3293
|
-
fi
|
|
3294
|
-
}
|
|
3295
|
-
|
|
3296
|
-
cmd_cleanup_stale() {
|
|
3297
|
-
local dry_run=0 arg
|
|
3298
|
-
for arg in "$@"; do
|
|
3299
|
-
case "$arg" in
|
|
3300
|
-
--dry-run) dry_run=1 ;;
|
|
3301
|
-
*)
|
|
3302
|
-
echo "aiur: cleanup-stale only accepts --dry-run" >&2
|
|
3303
|
-
exit 64
|
|
3304
|
-
;;
|
|
3305
|
-
esac
|
|
3306
|
-
done
|
|
3307
|
-
|
|
3308
|
-
aiur_resolve_identity
|
|
3309
|
-
if [ "$dry_run" -eq 1 ]; then
|
|
3310
|
-
report_stale_manual_smoke || true
|
|
3311
|
-
else
|
|
3312
|
-
report_stale_manual_smoke || true
|
|
3313
|
-
reap_stale_manual_smoke 1
|
|
3314
|
-
fi
|
|
3315
|
-
}
|
|
3316
|
-
|
|
3317
|
-
# `aiur set <key> <value>` — runtime config overrides without editing
|
|
3318
|
-
# `.aiur/config`. Currently: `aiur set max-agents N`.
|
|
3319
|
-
cmd_set() {
|
|
3320
|
-
local key="${1:-}"
|
|
3321
|
-
shift 2>/dev/null || true
|
|
3322
|
-
|
|
3323
|
-
case "$key" in
|
|
3324
|
-
max-agents)
|
|
3325
|
-
local n="${1:-}"
|
|
3326
|
-
if [ -z "$n" ] || [[ ! "$n" =~ ^[0-9]+$ ]] || [ "$n" -lt 1 ]; then
|
|
3327
|
-
echo "aiur: set max-agents expects a positive integer (e.g. aiur set max-agents 5)" >&2
|
|
3328
|
-
exit 64
|
|
3329
|
-
fi
|
|
3330
|
-
run_control_rpc "Aiur.AgentControlCLI.set_max_agents($n)"
|
|
3331
|
-
;;
|
|
3332
|
-
*)
|
|
3333
|
-
echo "aiur: unknown setting: ${key:-(none)} (supported: max-agents)" >&2
|
|
3334
|
-
exit 64
|
|
3335
|
-
;;
|
|
3336
|
-
esac
|
|
3337
|
-
}
|
|
3338
|
-
|
|
3339
|
-
sweep_dead_tmux_sockets() {
|
|
3340
|
-
local tmux_bin
|
|
3341
|
-
tmux_bin="$(command -v tmux || true)"
|
|
3342
|
-
[ -n "$tmux_bin" ] || return 0
|
|
3343
|
-
|
|
3344
|
-
local sockdir="${TMUX_TMPDIR:-/tmp}/tmux-$(id -u)"
|
|
3345
|
-
[ -d "$sockdir" ] || return 0
|
|
3346
|
-
|
|
3347
|
-
local removed=0 name path
|
|
3348
|
-
for path in "$sockdir"/*; do
|
|
3349
|
-
[ -S "$path" ] || continue
|
|
3350
|
-
name="${path##*/}"
|
|
3351
|
-
case "$name" in
|
|
3352
|
-
aiur* | *-driver | *-driver-*) ;;
|
|
3353
|
-
*) continue ;;
|
|
3354
|
-
esac
|
|
3355
|
-
if ! "$tmux_bin" -L "$name" list-sessions >/dev/null 2>&1; then
|
|
3356
|
-
rm -f "$path" 2>/dev/null && removed=$((removed + 1))
|
|
3357
|
-
fi
|
|
3358
|
-
done
|
|
3359
|
-
|
|
3360
|
-
[ "$removed" -gt 0 ] && echo "🧹 swept $removed dead aiur tmux socket(s)" >&2
|
|
3361
|
-
return 0
|
|
3362
|
-
}
|
|
3363
|
-
|
|
3364
|
-
# Reap stale aiur /tmp debris so a tmpfs /tmp can't fill from accumulated per-run
|
|
3365
|
-
# tempfiles and leaked test artifacts. Runs alongside sweep_dead_tmux_sockets on
|
|
3366
|
-
# foreground teardown (session_cleanup) and on `aiur stop`. Bounded and safe:
|
|
3367
|
-
#
|
|
3368
|
-
# * Only exact top-level Aiur artifact families under the temp roots the engine
|
|
3369
|
-
# and BEAM write to. Arbitrary Executor worktrees/checkouts like
|
|
3370
|
-
# `/tmp/aiur-pr490` are not candidates.
|
|
3371
|
-
# * Age-gated by AIUR_TMP_REAP_MINUTES (default 1440 = 24h). An entry is removed
|
|
3372
|
-
# only when NOTHING in its subtree was modified within the window, so a live
|
|
3373
|
-
# run's shared debug dir (aiur-rc / aiur-claude-hooks / aiur-debug) holding a
|
|
3374
|
-
# fresh file is spared even when the dir's own mtime is stale. A non-numeric or
|
|
3375
|
-
# 0 value disables the sweep.
|
|
3376
|
-
# * Ownership-gated: if anything in the tree is not owned by the effective user,
|
|
3377
|
-
# the whole tree is spared.
|
|
3378
|
-
# * A live run's pid-named session, agent, crash-ledger, and dump-baseline
|
|
3379
|
-
# handoffs are kept while its launcher pid is alive, regardless of age.
|
|
3380
|
-
is_aiur_tmp_artifact_candidate() {
|
|
3381
|
-
local base="$1" pid
|
|
3382
|
-
case "$base" in
|
|
3383
|
-
aiur-argv.* | aiur-startup.* | aiur-pane.* | aiur-launcher.* | aiur-capture.* | \
|
|
3384
|
-
aiur-trap.*.log | aiur-tree.*.json | aiur-wrapper.pid | aiur-*-frames.bin | \
|
|
3385
|
-
aiur-rc | aiur-claude-hooks | aiur-debug)
|
|
3386
|
-
return 0
|
|
3387
|
-
;;
|
|
3388
|
-
aiur-*-sessions | aiur-*-agents | aiur-*-alert-ledger | aiur-*-crash-dump-baseline)
|
|
3389
|
-
pid="${base#aiur-}"
|
|
3390
|
-
pid="${pid%-sessions}"
|
|
3391
|
-
pid="${pid%-agents}"
|
|
3392
|
-
pid="${pid%-alert-ledger}"
|
|
3393
|
-
pid="${pid%-crash-dump-baseline}"
|
|
3394
|
-
[ -n "$pid" ] && [ -z "${pid//[0-9]/}" ]
|
|
3395
|
-
return
|
|
3396
|
-
;;
|
|
3397
|
-
*)
|
|
3398
|
-
return 1
|
|
3399
|
-
;;
|
|
3400
|
-
esac
|
|
3401
|
-
}
|
|
3402
|
-
|
|
3403
|
-
sweep_stale_tmp_artifacts() {
|
|
3404
|
-
local minutes="${AIUR_TMP_REAP_MINUTES:-1440}"
|
|
3405
|
-
case "$minutes" in '' | *[!0-9]*) return 0 ;; esac
|
|
3406
|
-
[ "$minutes" -gt 0 ] || return 0
|
|
3407
|
-
|
|
3408
|
-
# The dirs the engine + BEAM target: ${TMPDIR:-/tmp} (argv/startup/pane tempfiles
|
|
3409
|
-
# plus the BEAM's System.tmp_dir debug dirs) and ${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}
|
|
3410
|
-
# (the session/agent pidfiles' session_root). Deduped — a typical box scans one dir.
|
|
3411
|
-
local roots=() d seen
|
|
3412
|
-
for d in "${TMPDIR:-/tmp}" "${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}"; do
|
|
3413
|
-
[ -d "$d" ] || continue
|
|
3414
|
-
seen=0
|
|
3415
|
-
local root
|
|
3416
|
-
if [ "${#roots[@]}" -gt 0 ]; then
|
|
3417
|
-
for root in "${roots[@]}"; do
|
|
3418
|
-
[ "$root" = "$d" ] && { seen=1; break; }
|
|
3419
|
-
done
|
|
3420
|
-
fi
|
|
3421
|
-
[ "$seen" -eq 1 ] || roots+=("$d")
|
|
3422
|
-
done
|
|
3423
|
-
[ "${#roots[@]}" -gt 0 ] || return 0
|
|
3424
|
-
|
|
3425
|
-
local removed=0 path base pid uid found
|
|
3426
|
-
uid="$(id -u)"
|
|
3427
|
-
for d in "${roots[@]}"; do
|
|
3428
|
-
while IFS= read -r -d '' path; do
|
|
3429
|
-
base="${path##*/}"
|
|
3430
|
-
is_aiur_tmp_artifact_candidate "$base" || continue
|
|
3431
|
-
|
|
3432
|
-
# Spare mixed-ownership trees; cleanup should never cross user boundaries.
|
|
3433
|
-
if ! found="$(find "$path" ! -user "$uid" -print -quit 2>/dev/null)"; then
|
|
3434
|
-
continue
|
|
3435
|
-
fi
|
|
3436
|
-
if [ -n "$found" ]; then
|
|
3437
|
-
continue
|
|
3438
|
-
fi
|
|
3439
|
-
# Spare anything modified within the window anywhere in its subtree.
|
|
3440
|
-
if ! found="$(find "$path" -mmin "-$minutes" -print -quit 2>/dev/null)"; then
|
|
3441
|
-
continue
|
|
3442
|
-
fi
|
|
3443
|
-
if [ -n "$found" ]; then
|
|
3444
|
-
continue
|
|
3445
|
-
fi
|
|
3446
|
-
# Spare a live run's pid-named bookkeeping and crash handoffs.
|
|
3447
|
-
case "$base" in
|
|
3448
|
-
aiur-*-sessions | aiur-*-agents | aiur-*-alert-ledger | aiur-*-crash-dump-baseline)
|
|
3449
|
-
pid="${base#aiur-}"
|
|
3450
|
-
pid="${pid%-sessions}"
|
|
3451
|
-
pid="${pid%-agents}"
|
|
3452
|
-
pid="${pid%-alert-ledger}"
|
|
3453
|
-
pid="${pid%-crash-dump-baseline}"
|
|
3454
|
-
if [ -n "$pid" ] && [ -z "${pid//[0-9]/}" ] && kill -0 "$pid" 2>/dev/null; then
|
|
3455
|
-
continue
|
|
3456
|
-
fi
|
|
3457
|
-
;;
|
|
3458
|
-
esac
|
|
3459
|
-
if rm -rf -- "$path" 2>/dev/null; then
|
|
3460
|
-
removed=$((removed + 1))
|
|
3461
|
-
fi
|
|
3462
|
-
done < <(find "$d" -maxdepth 1 -mindepth 1 -name 'aiur-*' -print0 2>/dev/null)
|
|
3463
|
-
done
|
|
3464
|
-
|
|
3465
|
-
[ "$removed" -gt 0 ] && echo "🧹 swept $removed stale aiur tmp artifact(s)" >&2
|
|
3466
|
-
return 0
|
|
3467
|
-
}
|
|
3468
|
-
|
|
3469
|
-
# A stop only reaps the daemon whose node name matches this project root's
|
|
3470
|
-
# instance key (keys are sha256(project_root), so instances can't reap each
|
|
3471
|
-
# other). A daemon launched from a different directory therefore survives a stop
|
|
3472
|
-
# invoked elsewhere — the silent orphan that keeps holding the dashboard port and
|
|
3473
|
-
# serving stale code/credentials. Surface it loudly (we warn, not reap, to
|
|
3474
|
-
# respect the deliberate isolation) so the operator can stop it explicitly.
|
|
3475
|
-
warn_other_aiur_daemons() {
|
|
3476
|
-
local self_node="$1" me pids pid cmd node port found=0
|
|
3477
|
-
me="${USER:-$(id -un 2>/dev/null)}"
|
|
3478
|
-
pids="$(pgrep -u "$me" -f -- "-name aiur-${me}" 2>/dev/null || true)"
|
|
3479
|
-
for pid in $pids; do
|
|
3480
|
-
cmd="$(pid_command "$pid")"
|
|
3481
|
-
case "$cmd" in *beam.smp*) : ;; *) continue ;; esac
|
|
3482
|
-
node="$(printf '%s' "$cmd" | grep -oE -- '-name [^ ]+' | awk '{print $2}' | head -1)"
|
|
3483
|
-
[ -z "$node" ] && continue
|
|
3484
|
-
[ "$node" = "$self_node" ] && continue
|
|
3485
|
-
port="$(ss -tlnpH 2>/dev/null | awk -v p="pid=$pid," '$0 ~ p {print $4}' \
|
|
3486
|
-
| grep -oE '[0-9]+$' | head -1 || true)"
|
|
3487
|
-
if [ "$found" -eq 0 ]; then
|
|
3488
|
-
echo "aiur: heads up — another aiur daemon is still running that this stop did not touch" >&2
|
|
3489
|
-
echo " (launched from a different directory, so it has a different instance key):" >&2
|
|
3490
|
-
found=1
|
|
3491
|
-
fi
|
|
3492
|
-
echo " pid=$pid node=$node${port:+ dashboard-port=$port} — stop it with: kill $pid" >&2
|
|
3493
|
-
done
|
|
3494
|
-
}
|
|
3495
|
-
|
|
3496
|
-
# Set when cmd_stop returns nonzero only because there was nothing to stop. It
|
|
3497
|
-
# lets a caller (cmd_restart) tell that benign outcome apart from a stop that
|
|
3498
|
-
# failed for a real reason, without changing `aiur stop`'s own exit code.
|
|
3499
|
-
AIUR_STOP_FOUND_NOTHING=0
|
|
3500
|
-
|
|
3501
|
-
# Stop the running session: kill this instance's tmux + BEAM, then sweep.
|
|
3502
|
-
cmd_stop() {
|
|
3503
|
-
AIUR_STOP_FOUND_NOTHING=0
|
|
3504
|
-
resolve_release
|
|
3505
|
-
aiur_resolve_identity
|
|
3506
|
-
RELEASE_NODE="$AIUR_RELEASE_NODE"
|
|
3507
|
-
ERL_EPMD_ADDRESS="${ERL_EPMD_ADDRESS:-127.0.0.1}"
|
|
3508
|
-
export RELEASE_NODE ERL_EPMD_ADDRESS
|
|
3509
|
-
resolve_control_identity_from_records
|
|
3510
|
-
|
|
3511
|
-
local workspace_root_file agent_pidfile
|
|
3512
|
-
workspace_root_file="$(workspace_root_file_from_instance_record 2>/dev/null || true)"
|
|
3513
|
-
agent_pidfile="$(agent_pidfile_from_instance_record 2>/dev/null || true)"
|
|
3514
|
-
|
|
3515
|
-
local tmux_bin
|
|
3516
|
-
tmux_bin="$(command -v tmux || true)"
|
|
3517
|
-
local session="${AIUR_ADOPTED_TMUX_SESSION:-${AIUR_SESSION_PREFIX}-${USER:-user}${AIUR_INSTANCE_KEY:+-$AIUR_INSTANCE_KEY}-default}"
|
|
3518
|
-
local socket="${AIUR_ADOPTED_TMUX_SOCKET:-${AIUR_SESSION_PREFIX}-${USER:-user}${AIUR_INSTANCE_KEY:+-$AIUR_INSTANCE_KEY}}"
|
|
3519
|
-
|
|
3520
|
-
local has_session=0
|
|
3521
|
-
if [ -n "$tmux_bin" ] && "$tmux_bin" -L "$socket" has-session -t "$session" 2>/dev/null; then
|
|
3522
|
-
has_session=1
|
|
3523
|
-
fi
|
|
3524
|
-
|
|
3525
|
-
if [ "${AIUR_CONTROL_ADOPTED_RECORD:-0}" -ne 1 ] && \
|
|
3526
|
-
[ "${AIUR_CONTROL_CURRENT_NODE_STATE:-}" = "down" ] && \
|
|
3527
|
-
[ "${AIUR_PROJECT_ROOT_SOURCE:-}" = "cwd" ] && \
|
|
3528
|
-
[ "$has_session" -eq 0 ] && \
|
|
3529
|
-
[ ! -f "$(aiur_crash_marker_path)" ]; then
|
|
3530
|
-
echo "aiur: no running aiur node at ${AIUR_RELEASE_NODE}; nothing stopped" >&2
|
|
3531
|
-
warn_other_aiur_daemons "$AIUR_RELEASE_NODE"
|
|
3532
|
-
print_global_config_control_hint
|
|
3533
|
-
AIUR_STOP_FOUND_NOTHING=1
|
|
3534
|
-
return 1
|
|
3535
|
-
fi
|
|
3536
|
-
|
|
3537
|
-
# Tell the background BEAM-death watchdog this exit is intentional before we
|
|
3538
|
-
# kill the BEAM, so it consumes the sentinel instead of recording a crash. The
|
|
3539
|
-
# watchdog removes the sentinel when it fires; a fresh start also clears it.
|
|
3540
|
-
# Clear any prior crash marker too — `status` should report a clean stop, not
|
|
3541
|
-
# a stale orphan from an earlier dead run.
|
|
3542
|
-
mkdir -p "$AIUR_BG_STATE_DIR" 2>/dev/null || true
|
|
3543
|
-
: >"$(aiur_stop_sentinel_path)" 2>/dev/null || true
|
|
3544
|
-
rm -f "$(aiur_crash_marker_path)" 2>/dev/null || true
|
|
3545
|
-
|
|
3546
|
-
if [ -n "$tmux_bin" ]; then
|
|
3547
|
-
"$tmux_bin" -L "$socket" kill-session -t "$session" 2>/dev/null || true
|
|
3548
|
-
fi
|
|
3549
|
-
|
|
3550
|
-
# Reap any BEAM holding our node name regardless of which release dir launched
|
|
3551
|
-
# it. Do not sweep by release dir: sibling instances share dev releases. Use
|
|
3552
|
-
# the generous stop TERM grace: the BEAM, once TERM'd, runs its own graceful
|
|
3553
|
-
# shutdown (ProcessReaper reaping the agent tree, opencode session deletes,
|
|
3554
|
-
# then the BEAM-side workspace sweep) and the short 3s startup default would
|
|
3555
|
-
# SIGKILL it mid-cleanup, orphaning exactly the agents this stop must reap.
|
|
3556
|
-
# The loop exits the moment the BEAM is gone, so a fast stop costs nothing.
|
|
3557
|
-
kill_beams_matching "-name ${AIUR_RELEASE_NODE}" 300
|
|
3558
|
-
|
|
3559
|
-
# Final cwd-sweep backstop after the BEAM is dead. The launcher's instance
|
|
3560
|
-
# record points to the BEAM-written root handoff, so degraded stop never has
|
|
3561
|
-
# to wait for an overloaded daemon before it starts tearing workers down.
|
|
3562
|
-
reap_workspace_cwd_from_file "$workspace_root_file"
|
|
3563
|
-
|
|
3564
|
-
# The BEAM (alive until the TERM above) reaped its own headless agents through
|
|
3565
|
-
# ProcessReaper on Application.stop. kill-server is the guarantee the earlier
|
|
3566
|
-
# kill-session can't give for mid-turn agents: every pane agent across all
|
|
3567
|
-
# windows dies and no live aiur tmux server is left behind.
|
|
3568
|
-
if [ -n "$tmux_bin" ]; then
|
|
3569
|
-
"$tmux_bin" -L "$socket" kill-server 2>/dev/null || true
|
|
3570
|
-
fi
|
|
3571
|
-
|
|
3572
|
-
# Belt-and-suspenders for this run's headless agents after the BEAM exits.
|
|
3573
|
-
# The instance record owns its pidfile; a global sweep can kill live agents
|
|
3574
|
-
# belonging to another instance that shares the runtime directory.
|
|
3575
|
-
if [ -n "$agent_pidfile" ] && [ -e "$agent_pidfile" ]; then
|
|
3576
|
-
reap_aiur_agents "" "$agent_pidfile"
|
|
3577
|
-
rm -f "$agent_pidfile" 2>/dev/null || true
|
|
3578
|
-
fi
|
|
3579
|
-
|
|
3580
|
-
reap_stale_manual_smoke 0
|
|
3581
|
-
sweep_dead_tmux_sockets
|
|
3582
|
-
sweep_stale_tmp_artifacts
|
|
3583
|
-
rm -f "$(aiur_instance_record_path)" 2>/dev/null || true
|
|
3584
|
-
warn_other_aiur_daemons "$AIUR_RELEASE_NODE"
|
|
3585
|
-
}
|
|
3586
|
-
|
|
3587
|
-
# Stop, refresh the release, start again — `aiur restart`.
|
|
3588
|
-
#
|
|
3589
|
-
# The refresh deliberately runs BETWEEN the stop and the start. A dev rebuild
|
|
3590
|
-
# rewrites the release directory in place, and a BEAM booted from that directory
|
|
3591
|
-
# is exactly what the control-retry path (EX_TEMPFAIL) exists to survive; doing
|
|
3592
|
-
# it while the daemon is down means the new daemon boots from a release nothing
|
|
3593
|
-
# else is holding half-written.
|
|
3594
|
-
#
|
|
3595
|
-
# The engine itself never knows how to build: the installed CLI runs a pinned
|
|
3596
|
-
# platform release with no source tree beside it, so its restart is a plain
|
|
3597
|
-
# bounce. The dev shim supplies its build-if-stale step through
|
|
3598
|
-
# AIUR_RESTART_BUILD_CMD, a shell command line run via `bash -c` so the wrapper
|
|
3599
|
-
# (not this parser) owns quoting.
|
|
3600
|
-
aiur_restart_daemon_state="untouched"
|
|
3601
|
-
|
|
3602
|
-
# Everything between the stop and a confirmed start runs with this armed, so the
|
|
3603
|
-
# operator hears about a fleet that is down no matter which step failed — a
|
|
3604
|
-
# rebuild, a missing tmux, a BEAM that would not boot, or a Ctrl-C during the
|
|
3605
|
-
# minutes-long rebuild. Naming only the step that failed would let "aiur failed
|
|
3606
|
-
# to start" read as "nothing changed", which is the silently stopped fleet.
|
|
3607
|
-
aiur_restart_report_state() {
|
|
3608
|
-
[ "$aiur_restart_daemon_state" = "stopped" ] || return 0
|
|
3609
|
-
|
|
3610
|
-
# release_dir is resolved by the stop; fall back to the caller's declared dir
|
|
3611
|
-
# for the paths that fail before that.
|
|
3612
|
-
local dir="${release_dir:-${AIUR_RELEASE_DIR:-}}"
|
|
3613
|
-
|
|
3614
|
-
echo "aiur: the daemon is STOPPED and was NOT restarted" >&2
|
|
3615
|
-
if [ -n "$dir" ] && [ -x "$dir/bin/aiur" ]; then
|
|
3616
|
-
echo "aiur: fix the cause and run 'aiur restart' again, or 'aiur restart --no-build' to start" >&2
|
|
3617
|
-
echo " on the release already on disk" >&2
|
|
23
|
+
set -euo pipefail
|
|
24
|
+
# Raise the soft open-file limit toward the hard maximum. High agent concurrency
|
|
25
|
+
# spawns many tmux/opencode/git subprocesses + sockets; on hosts with a low
|
|
26
|
+
# default (macOS ships 256) that exhausts file descriptors (:emfile) and crashes
|
|
27
|
+
# the node. Soft<=hard needs no privilege; best-effort, never fatal.
|
|
28
|
+
__aiur_hard_nofile="$(ulimit -Hn 2>/dev/null || echo)"
|
|
29
|
+
if [ "${__aiur_hard_nofile}" = "unlimited" ]; then
|
|
30
|
+
ulimit -Sn 65536 2>/dev/null || true
|
|
31
|
+
elif [ -n "${__aiur_hard_nofile}" ]; then
|
|
32
|
+
ulimit -Sn "${__aiur_hard_nofile}" 2>/dev/null || true
|
|
33
|
+
fi
|
|
34
|
+
unset __aiur_hard_nofile
|
|
35
|
+
# Export the effective soft limit after the best-effort raise. The BEAM uses
|
|
36
|
+
# this inherited value for FD-headroom admission on hosts without procfs,
|
|
37
|
+
# avoiding a runtime `ulimit` subprocess precisely when descriptors are scarce.
|
|
38
|
+
__aiur_soft_nofile="$(ulimit -Sn 2>/dev/null || echo)"
|
|
39
|
+
if [[ "$__aiur_soft_nofile" =~ ^[0-9]+$ ]]; then
|
|
40
|
+
export AIUR_NOFILE_SOFT_LIMIT="$__aiur_soft_nofile"
|
|
41
|
+
else
|
|
42
|
+
unset AIUR_NOFILE_SOFT_LIMIT
|
|
43
|
+
fi
|
|
44
|
+
unset __aiur_soft_nofile
|
|
45
|
+
# Preserve the shell that initiated the run as a best-effort Executor root.
|
|
46
|
+
# An explicit positive override wins (service managers may know a better root);
|
|
47
|
+
# otherwise the engine's parent is the nearest identity available before tmux
|
|
48
|
+
# hands daemon ownership to its pane launcher.
|
|
49
|
+
if ! [[ "${AIUR_OPERATOR_PID:-}" =~ ^[1-9][0-9]*$ ]]; then
|
|
50
|
+
if [[ "${PPID:-}" =~ ^[1-9][0-9]*$ ]]; then
|
|
51
|
+
export AIUR_OPERATOR_PID="$PPID"
|
|
3618
52
|
else
|
|
3619
|
-
|
|
3620
|
-
# there is nothing for --no-build to start. Advertising it anyway would cost
|
|
3621
|
-
# the operator a second failed cycle while the fleet stays down.
|
|
3622
|
-
echo "aiur: no complete release is left on disk, so --no-build has nothing to start —" >&2
|
|
3623
|
-
echo " fix the build and run 'aiur restart' again" >&2
|
|
3624
|
-
fi
|
|
3625
|
-
}
|
|
3626
|
-
|
|
3627
|
-
# The rebuild runs in a process this command cannot see into, so "the builder
|
|
3628
|
-
# exited 0" is not the same claim as "the release I am about to boot is the one
|
|
3629
|
-
# it just built" — a wrapper pointed at another checkout satisfies the first and
|
|
3630
|
-
# not the second, which is exactly how a bounce ships a stale release while every
|
|
3631
|
-
# step reports success. The builder leaves a receipt naming the release dir it
|
|
3632
|
-
# vouches for and the commit it built from; this checks both against the release
|
|
3633
|
-
# on disk. Anything unverifiable is reported as unverifiable, never as success.
|
|
3634
|
-
verify_restart_build() {
|
|
3635
|
-
local receipt="$1"
|
|
3636
|
-
local built_dir built_sha stamped_sha stamp target_dir
|
|
3637
|
-
|
|
3638
|
-
if [ ! -s "$receipt" ]; then
|
|
3639
|
-
echo "aiur: the rebuild left no build receipt, so restart cannot confirm which" >&2
|
|
3640
|
-
echo " release it produced" >&2
|
|
3641
|
-
return 1
|
|
3642
|
-
fi
|
|
3643
|
-
|
|
3644
|
-
built_dir="$(sed -n 's/^release_dir=//p' "$receipt" | head -n 1)"
|
|
3645
|
-
built_sha="$(sed -n 's/^source_sha=//p' "$receipt" | head -n 1)"
|
|
3646
|
-
|
|
3647
|
-
# Fail on the missing field rather than on whatever an empty one happens to
|
|
3648
|
-
# compare equal to further down.
|
|
3649
|
-
if [ -z "$built_dir" ] || [ -z "$built_sha" ]; then
|
|
3650
|
-
echo "aiur: the build receipt is malformed; restart cannot confirm what was built" >&2
|
|
3651
|
-
return 1
|
|
3652
|
-
fi
|
|
3653
|
-
|
|
3654
|
-
# Compare canonical paths: the builder and this engine can reach the same
|
|
3655
|
-
# release through different symlinked parents.
|
|
3656
|
-
built_dir="$(cd -P "$built_dir" 2>/dev/null && pwd || printf '%s' "$built_dir")"
|
|
3657
|
-
target_dir="$(cd -P "${AIUR_RELEASE_DIR:-}" 2>/dev/null && pwd || printf '%s' "${AIUR_RELEASE_DIR:-}")"
|
|
3658
|
-
|
|
3659
|
-
if [ "$built_dir" != "$target_dir" ]; then
|
|
3660
|
-
echo "aiur: the rebuild targeted a different release than the one about to boot" >&2
|
|
3661
|
-
echo " rebuilt : $built_dir" >&2
|
|
3662
|
-
echo " booting : $target_dir" >&2
|
|
3663
|
-
return 1
|
|
3664
|
-
fi
|
|
3665
|
-
|
|
3666
|
-
stamp="$target_dir/AIUR_BUILD_STAMP"
|
|
3667
|
-
if [ ! -r "$stamp" ]; then
|
|
3668
|
-
echo "aiur: the release carries no build stamp, so restart cannot confirm it is" >&2
|
|
3669
|
-
echo " the one just built ($stamp)" >&2
|
|
3670
|
-
return 1
|
|
3671
|
-
fi
|
|
3672
|
-
|
|
3673
|
-
stamped_sha="$(sed -n 's/^source_sha=//p' "$stamp" | head -n 1)"
|
|
3674
|
-
if [ "$stamped_sha" != "$built_sha" ]; then
|
|
3675
|
-
echo "aiur: the release on disk was not built from the commit the rebuild reported" >&2
|
|
3676
|
-
echo " rebuild reported : $built_sha" >&2
|
|
3677
|
-
echo " release stamped : $stamped_sha" >&2
|
|
3678
|
-
return 1
|
|
3679
|
-
fi
|
|
3680
|
-
|
|
3681
|
-
# Say what was actually established. "verified ... built from unknown" would be
|
|
3682
|
-
# a silent-pass guard reporting success over a release it cannot identify, and
|
|
3683
|
-
# a dirty tree means the SHA is a floor rather than an identity.
|
|
3684
|
-
if [ "$stamped_sha" = "unknown" ]; then
|
|
3685
|
-
echo "aiur: release $target_dir matches the rebuild, but its source commit is" >&2
|
|
3686
|
-
echo " unknown (built outside a git work tree) — provenance unverified" >&2
|
|
3687
|
-
return 0
|
|
3688
|
-
fi
|
|
3689
|
-
|
|
3690
|
-
if [ "$(sed -n 's/^dirty=//p' "$stamp" | head -n 1)" = "yes" ]; then
|
|
3691
|
-
echo "aiur: release $target_dir built from $stamped_sha with uncommitted changes" >&2
|
|
3692
|
-
return 0
|
|
3693
|
-
fi
|
|
3694
|
-
|
|
3695
|
-
echo "aiur: verified release $target_dir built from $stamped_sha" >&2
|
|
3696
|
-
}
|
|
3697
|
-
|
|
3698
|
-
cmd_restart() {
|
|
3699
|
-
local skip_build=0 arg
|
|
3700
|
-
local run_args=()
|
|
3701
|
-
|
|
3702
|
-
# Refuse a stale config before stopping a healthy daemon. dispatch_run also
|
|
3703
|
-
# checks this for ordinary launches, but restart mutates state before it gets
|
|
3704
|
-
# that far.
|
|
3705
|
-
reject_legacy_config "$@"
|
|
3706
|
-
|
|
3707
|
-
for arg in "$@"; do
|
|
3708
|
-
case "$arg" in
|
|
3709
|
-
--no-build) skip_build=1 ;;
|
|
3710
|
-
*) run_args+=("$arg") ;;
|
|
3711
|
-
esac
|
|
3712
|
-
done
|
|
3713
|
-
|
|
3714
|
-
# Restarting something that is already down is a start, not an error: the
|
|
3715
|
-
# operator asked for a running daemon on current code, and that is what they
|
|
3716
|
-
# get. Any OTHER stop failure is fatal — building and starting on top of a
|
|
3717
|
-
# half-stopped instance is the in-place rewrite this ordering exists to avoid.
|
|
3718
|
-
local stop_status=0
|
|
3719
|
-
cmd_stop || stop_status=$?
|
|
3720
|
-
if [ "$stop_status" -ne 0 ]; then
|
|
3721
|
-
if [ "${AIUR_STOP_FOUND_NOTHING:-0}" -eq 1 ]; then
|
|
3722
|
-
echo "aiur: nothing was running; restart will start a fresh session" >&2
|
|
3723
|
-
else
|
|
3724
|
-
die "restart aborted: stop failed (exit $stop_status); the daemon may still be running"
|
|
3725
|
-
fi
|
|
3726
|
-
fi
|
|
3727
|
-
|
|
3728
|
-
# cmd_stop's tmux teardown is best-effort (`|| true`), so a stop can report
|
|
3729
|
-
# success while the session survives. Starting from there would rebuild the
|
|
3730
|
-
# release under a live BEAM and then hit run_session's idempotent "already
|
|
3731
|
-
# running" return — exit 0, stale release, no bounce. Refuse instead.
|
|
3732
|
-
# release_bin is unset only when the stop was stubbed out, i.e. in tests.
|
|
3733
|
-
if [ -n "${release_bin:-}" ] && [ "$(probe_control_liveness)" = "up" ]; then
|
|
3734
|
-
die "restart aborted: the previous session still answers after the stop; nothing was rebuilt or restarted"
|
|
3735
|
-
fi
|
|
3736
|
-
|
|
3737
|
-
aiur_restart_daemon_state="stopped"
|
|
3738
|
-
trap aiur_restart_report_state EXIT INT TERM
|
|
3739
|
-
|
|
3740
|
-
if [ -n "${AIUR_RESTART_BUILD_CMD:-}" ] && [ "$skip_build" -eq 0 ]; then
|
|
3741
|
-
local build_status=0 receipt
|
|
3742
|
-
receipt="$(mktemp "${TMPDIR:-/tmp}/aiur-restart-receipt.XXXXXX")"
|
|
3743
|
-
AIUR_RESTART_BUILD_RECEIPT="$receipt" bash -c "$AIUR_RESTART_BUILD_CMD" || build_status=$?
|
|
3744
|
-
if [ "$build_status" -ne 0 ]; then
|
|
3745
|
-
# Exit with the builder's own status rather than a flat 1, so a caller can
|
|
3746
|
-
# still tell a failed rebuild from any other abort.
|
|
3747
|
-
rm -f "$receipt"
|
|
3748
|
-
echo "❌ restart aborted before start: the rebuild failed" >&2
|
|
3749
|
-
aiur_restart_report_state
|
|
3750
|
-
trap - EXIT INT TERM
|
|
3751
|
-
exit "$build_status"
|
|
3752
|
-
fi
|
|
3753
|
-
|
|
3754
|
-
# A builder that declares AIUR_RESTART_BUILD_VERIFIES has promised a receipt,
|
|
3755
|
-
# so a missing or contradicted one is a real failure and an unverifiable
|
|
3756
|
-
# rebuild is not a lesser success — starting anyway would hand back the
|
|
3757
|
-
# stale-release outcome this ordering exists to prevent, with a "restarted"
|
|
3758
|
-
# line on top of it. A builder that makes no such promise cannot be held to
|
|
3759
|
-
# it; say plainly that the guarantee did not apply rather than convert an
|
|
3760
|
-
# unknown wrapper into a stopped fleet.
|
|
3761
|
-
if [ "${AIUR_RESTART_BUILD_VERIFIES:-0}" = "1" ]; then
|
|
3762
|
-
local verify_status=0
|
|
3763
|
-
verify_restart_build "$receipt" || verify_status=$?
|
|
3764
|
-
rm -f "$receipt"
|
|
3765
|
-
if [ "$verify_status" -ne 0 ]; then
|
|
3766
|
-
echo "❌ restart aborted before start: the rebuild could not be verified" >&2
|
|
3767
|
-
# Naming the builder makes an inherited AIUR_RESTART_BUILD_CMD — a dev
|
|
3768
|
-
# shim leaking into another shell — diagnosable from this output alone,
|
|
3769
|
-
# instead of an identical refusal on every retry.
|
|
3770
|
-
echo " rebuild command: ${AIUR_RESTART_BUILD_CMD}" >&2
|
|
3771
|
-
echo " Rebuild from the checkout you mean and retry, or 'aiur restart --no-build'" >&2
|
|
3772
|
-
echo " to start the release on disk without the guarantee." >&2
|
|
3773
|
-
aiur_restart_report_state
|
|
3774
|
-
trap - EXIT INT TERM
|
|
3775
|
-
exit 70
|
|
3776
|
-
fi
|
|
3777
|
-
else
|
|
3778
|
-
rm -f "$receipt"
|
|
3779
|
-
echo "aiur: UNVERIFIED rebuild — this builder reports no receipt, so restart" >&2
|
|
3780
|
-
echo " cannot confirm the release it is about to boot is the one just built" >&2
|
|
3781
|
-
fi
|
|
3782
|
-
elif [ "$skip_build" -eq 1 ]; then
|
|
3783
|
-
echo "aiur: --no-build — restarting on the release already on disk" >&2
|
|
3784
|
-
fi
|
|
3785
|
-
|
|
3786
|
-
# Always detached: the daemon this stopped was itself detached, and a restart
|
|
3787
|
-
# that captured the terminal would be a surprise. `--interactive` still opts
|
|
3788
|
-
# into the attachable session (`aiur --bg --interactive`), and a purely
|
|
3789
|
-
# foreground restart remains `aiur stop && aiur run`.
|
|
3790
|
-
dispatch_run --bg "${run_args[@]+"${run_args[@]}"}"
|
|
3791
|
-
|
|
3792
|
-
aiur_restart_daemon_state="running"
|
|
3793
|
-
trap - EXIT INT TERM
|
|
3794
|
-
}
|
|
3795
|
-
|
|
3796
|
-
# --- upgrade version notice + aiur upgrade -----------------------------------
|
|
3797
|
-
|
|
3798
|
-
# Extract a top-level string field from a JSON document. The notice keys we
|
|
3799
|
-
# read (`text`, `available`, `channel`, `channel_gone`) are unique to the
|
|
3800
|
-
# notice, and the dist-tags keys we read are the tag names themselves, so a
|
|
3801
|
-
# plain sed over the document is unambiguous. Handles both quoted strings
|
|
3802
|
-
# (`"channel":"nightly"`) and unquoted scalars (`"channel_gone":true`,
|
|
3803
|
-
# `"available":null`) — the state file serializes booleans/null without quotes.
|
|
3804
|
-
upgrade_state_string() {
|
|
3805
|
-
local file="$1" key="$2"
|
|
3806
|
-
sed -n "s/.*\"$key\": *\"\([^\"]*\)\".*/\1/p; s/.*\"$key\": *\([^,}]*\).*/\1/p" "$file" | head -n 1
|
|
3807
|
-
}
|
|
3808
|
-
|
|
3809
|
-
# Atomically write the "last told" marker (a single line). The engine owns this
|
|
3810
|
-
# file; the daemon only reads it for its no-repeat decision. Best-effort and
|
|
3811
|
-
# always succeeds (returns 0) so a read-only/unwritable state dir can never
|
|
3812
|
-
# abort the caller — the notice display and `aiur upgrade` must not fail
|
|
3813
|
-
# because a marker could not be written.
|
|
3814
|
-
upgrade_state_mark_notified() {
|
|
3815
|
-
local value="$1" file="$AIUR_BG_STATE_DIR/upgrade-notified.txt" tmp
|
|
3816
|
-
mkdir -p "$AIUR_BG_STATE_DIR" 2>/dev/null || return 0
|
|
3817
|
-
tmp="$(mktemp "$AIUR_BG_STATE_DIR/upgrade-notified.XXXXXX" 2>/dev/null)" || return 0
|
|
3818
|
-
printf '%s\n' "$value" >"$tmp" 2>/dev/null
|
|
3819
|
-
mv -f "$tmp" "$file" 2>/dev/null
|
|
3820
|
-
rm -f "$tmp" 2>/dev/null
|
|
3821
|
-
return 0
|
|
3822
|
-
}
|
|
3823
|
-
|
|
3824
|
-
# Surface the pending upgrade notice, if any, to the operator's terminal. This
|
|
3825
|
-
# is a cheap local state read (the daemon's `Aiur.Upgrade` check writes it
|
|
3826
|
-
# during boot) — never the registry — so it can never delay startup. It honors
|
|
3827
|
-
# the same opt-outs as the check, prints only once per version (the `notified`
|
|
3828
|
-
# marker), and is completely silent under a development launcher.
|
|
3829
|
-
maybe_surface_upgrade_notice() {
|
|
3830
|
-
aiur_resolve_identity
|
|
3831
|
-
|
|
3832
|
-
# A development launcher (aiurdev) must never suggest an upgrade: the notice
|
|
3833
|
-
# would be wrong (the local tree is routinely ahead of every published
|
|
3834
|
-
# version) and acting on it would overwrite a working tree. Detect the
|
|
3835
|
-
# launcher, never the directory — an installed aiur can be run from inside a
|
|
3836
|
-
# clone and still deserves its notice.
|
|
3837
|
-
case "${AIUR_RELEASE_DIR:-}" in
|
|
3838
|
-
*/src/_build/dev/rel/aiur) return 0 ;;
|
|
3839
|
-
esac
|
|
3840
|
-
|
|
3841
|
-
# Env opt-outs, mirroring Aiur.Upgrade.disabled?/0.
|
|
3842
|
-
case "${AIUR_UPGRADE_CHECK_DISABLED:-}" in 1 | true) return 0 ;; esac
|
|
3843
|
-
case "${AIUR_NO_UPDATE_NOTIFIER:-}" in 1 | true) return 0 ;; esac
|
|
3844
|
-
case "${CI:-}" in "" | false | 0) ;; *) return 0 ;; esac
|
|
3845
|
-
|
|
3846
|
-
# Interactive gate: never print into a piped or non-TTY stream.
|
|
3847
|
-
[ -t 2 ] || return 0
|
|
3848
|
-
|
|
3849
|
-
maybe_print_upgrade_notice
|
|
3850
|
-
}
|
|
3851
|
-
|
|
3852
|
-
# The display half of the notice: read the daemon-written state, decide whether
|
|
3853
|
-
# the operator has already been told this version, print it to stderr, and mark
|
|
3854
|
-
# it told. Split from `maybe_surface_upgrade_notice` (the env/CI/dev/TTY gates)
|
|
3855
|
-
# so the print/decide logic is testable without a pty.
|
|
3856
|
-
maybe_print_upgrade_notice() {
|
|
3857
|
-
local state_file="$AIUR_BG_STATE_DIR/upgrade.json"
|
|
3858
|
-
[ -r "$state_file" ] || return 0
|
|
3859
|
-
|
|
3860
|
-
local text available channel channel_gone notified installed
|
|
3861
|
-
text="$(upgrade_state_string "$state_file" text)"
|
|
3862
|
-
[ -n "$text" ] || return 0
|
|
3863
|
-
available="$(upgrade_state_string "$state_file" available)"
|
|
3864
|
-
channel="$(upgrade_state_string "$state_file" channel)"
|
|
3865
|
-
channel_gone="$(upgrade_state_string "$state_file" channel_gone)"
|
|
3866
|
-
notified="$(cat "$AIUR_BG_STATE_DIR/upgrade-notified.txt" 2>/dev/null || true)"
|
|
3867
|
-
installed="$(cli_package_version || true)"
|
|
3868
|
-
|
|
3869
|
-
if [ "$channel_gone" = "true" ]; then
|
|
3870
|
-
# A "channel no longer publishes" notice: say it once, never nag.
|
|
3871
|
-
[ -n "$channel" ] && [ "$notified" = "gone:${channel}" ] && return 0
|
|
3872
|
-
printf '%s\n' "$text" >&2
|
|
3873
|
-
upgrade_state_mark_notified "gone:${channel}"
|
|
3874
|
-
return 0
|
|
3875
|
-
fi
|
|
3876
|
-
|
|
3877
|
-
# A normal notice: only when it is genuinely newer than what is installed
|
|
3878
|
-
# (never a downgrade) and the operator has not already been told this exact
|
|
3879
|
-
# version.
|
|
3880
|
-
[ -n "$available" ] || return 0
|
|
3881
|
-
[ -n "$installed" ] || return 0
|
|
3882
|
-
version_is_older "$installed" "$available" || return 0
|
|
3883
|
-
if [ -n "$notified" ] && ! version_is_older "$notified" "$available"; then
|
|
3884
|
-
return 0
|
|
3885
|
-
fi
|
|
3886
|
-
|
|
3887
|
-
printf '%s\n' "$text" >&2
|
|
3888
|
-
upgrade_state_mark_notified "$available"
|
|
3889
|
-
}
|
|
3890
|
-
|
|
3891
|
-
# Detect how this `aiur` was installed from the launcher-resolved release dir
|
|
3892
|
-
# and the package layout — never from the working directory, so an installed
|
|
3893
|
-
# aiur run from inside a clone is still recognized as an npm install.
|
|
3894
|
-
aiur_install_method() {
|
|
3895
|
-
case "${AIUR_RELEASE_DIR:-}" in
|
|
3896
|
-
*/src/_build/dev/rel/aiur)
|
|
3897
|
-
printf 'dev'
|
|
3898
|
-
return
|
|
3899
|
-
;;
|
|
3900
|
-
esac
|
|
3901
|
-
if command -v brew >/dev/null 2>&1 && brew list aiur >/dev/null 2>&1; then
|
|
3902
|
-
printf 'brew'
|
|
3903
|
-
return
|
|
3904
|
-
fi
|
|
3905
|
-
if [ -r "$engine_dir/../package.json" ]; then
|
|
3906
|
-
printf 'npm'
|
|
3907
|
-
return
|
|
53
|
+
unset AIUR_OPERATOR_PID
|
|
3908
54
|
fi
|
|
3909
|
-
|
|
55
|
+
fi
|
|
56
|
+
die() {
|
|
57
|
+
echo "❌ $*" >&2
|
|
58
|
+
exit 1
|
|
3910
59
|
}
|
|
3911
60
|
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
#
|
|
3916
|
-
#
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
esac
|
|
3925
|
-
done
|
|
3926
|
-
|
|
3927
|
-
aiur_resolve_identity
|
|
3928
|
-
|
|
3929
|
-
case "$(aiur_install_method)" in
|
|
3930
|
-
dev)
|
|
3931
|
-
die "aiur upgrade cannot run from a development checkout (aiurdev): the local tree is not a published install, so upgrade would not touch this working tree and nothing has been changed. Install aiur from npm and run \`aiur upgrade\` from the installed CLI."
|
|
3932
|
-
;;
|
|
3933
|
-
brew)
|
|
3934
|
-
die "aiur upgrade does not support Homebrew installs: aiur releases are npm-only right now (the Homebrew tap is not fed by releases). Reinstall from npm (\`npm install -g aiur-cli\`) and use \`aiur upgrade\` there."
|
|
3935
|
-
;;
|
|
3936
|
-
unknown)
|
|
3937
|
-
die "aiur could not determine how it was installed, so upgrade refused to guess. Reinstall from npm (\`npm install -g aiur-cli\`) and retry."
|
|
3938
|
-
;;
|
|
3939
|
-
esac
|
|
3940
|
-
|
|
3941
|
-
resolve_release
|
|
3942
|
-
prepare_distribution || die "distribution setup failed; cannot check for a running daemon"
|
|
3943
|
-
|
|
3944
|
-
# Do not swap the binary underneath a running daemon or a live fleet.
|
|
3945
|
-
if [ "$(probe_control_liveness)" = "up" ]; then
|
|
3946
|
-
if [ "$force" -ne 1 ]; then
|
|
3947
|
-
echo "❌ aiur is running. \`aiur upgrade\` refuses to replace the binary under a live daemon." >&2
|
|
3948
|
-
echo " Stop it first (\`aiur stop\`), upgrade, then start again; or pass --force to upgrade" >&2
|
|
3949
|
-
echo " anyway. With --force the running daemon keeps the old code until restarted, and" >&2
|
|
3950
|
-
echo " in-flight agents are unaffected until then." >&2
|
|
3951
|
-
exit 64
|
|
3952
|
-
fi
|
|
3953
|
-
echo "⚠️ aiur is running; --force upgrades anyway. The running daemon keeps the old" >&2
|
|
3954
|
-
echo " code until you restart it — in-flight agents are unaffected until then." >&2
|
|
3955
|
-
fi
|
|
3956
|
-
|
|
3957
|
-
local before after channel available tags latest next nightly
|
|
3958
|
-
before="$(cli_package_version || true)"
|
|
3959
|
-
[ -n "$before" ] || die "aiur could not read the installed version; upgrade aborted"
|
|
3960
|
-
|
|
3961
|
-
if ! tags="$(npm view aiur-cli dist-tags --json 2>/dev/null)"; then
|
|
3962
|
-
die "aiur could not reach the npm registry to determine available versions (is npm installed and online?); upgrade aborted"
|
|
3963
|
-
fi
|
|
3964
|
-
|
|
3965
|
-
# Resolve the user's channel from the installed version AND the observed
|
|
3966
|
-
# dist-tags, mirroring Aiur.Upgrade.channel/2. A `next` user must never be
|
|
3967
|
-
# measured against (or "upgraded" to) `latest` when that is lower — today
|
|
3968
|
-
# latest=0.0.3, next=0.0.4, nightly=0.0.5-nightly.<sha>.
|
|
3969
|
-
case "$before" in
|
|
3970
|
-
*-nightly*)
|
|
3971
|
-
channel="nightly"
|
|
3972
|
-
;;
|
|
3973
|
-
*)
|
|
3974
|
-
next="$(upgrade_state_string <(printf '%s' "$tags") next || true)"
|
|
3975
|
-
latest="$(upgrade_state_string <(printf '%s' "$tags") latest || true)"
|
|
3976
|
-
if [ -n "$next" ] &&
|
|
3977
|
-
{ [ "$before" = "$next" ] ||
|
|
3978
|
-
{ [ -n "$latest" ] && version_is_older "$before" "$next" && version_is_older "$latest" "$before"; }; }; then
|
|
3979
|
-
channel="next"
|
|
3980
|
-
else
|
|
3981
|
-
channel="latest"
|
|
3982
|
-
fi
|
|
3983
|
-
;;
|
|
3984
|
-
esac
|
|
3985
|
-
|
|
3986
|
-
available="$(upgrade_state_string <(printf '%s' "$tags") "$channel" || true)"
|
|
3987
|
-
if [ -z "$available" ]; then
|
|
3988
|
-
echo "❌ the ${channel} channel no longer publishes new versions (installed ${before})." >&2
|
|
3989
|
-
echo " aiur upgrade will not silently fall back to another channel." >&2
|
|
3990
|
-
exit 64
|
|
3991
|
-
fi
|
|
3992
|
-
|
|
3993
|
-
if ! version_is_older "$before" "$available"; then
|
|
3994
|
-
echo "aiur is already up to date on the ${channel} channel (${before})." >&2
|
|
3995
|
-
return 0
|
|
3996
|
-
fi
|
|
3997
|
-
|
|
3998
|
-
echo "aiur: upgrading aiur-cli from ${before} to ${available} on the ${channel} channel..." >&2
|
|
3999
|
-
if ! npm install -g "aiur-cli@${channel}" --no-offline --no-prefer-offline; then
|
|
4000
|
-
die "aiur upgrade failed during \`npm install -g aiur-cli@${channel}\`; the installed version is unchanged"
|
|
4001
|
-
fi
|
|
4002
|
-
|
|
4003
|
-
after="$(cli_package_version || true)"
|
|
4004
|
-
if [ -n "$after" ] && [ "$after" != "$before" ]; then
|
|
4005
|
-
echo "aiur upgraded from ${before} to ${after}." >&2
|
|
4006
|
-
else
|
|
4007
|
-
echo "aiur upgraded on the ${channel} channel (was ${before})." >&2
|
|
4008
|
-
fi
|
|
61
|
+
engine_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
62
|
+
engine_source="${BASH_SOURCE[0]}"
|
|
63
|
+
|
|
64
|
+
# Every other function lives in a sibling module. A missing one fails here, by
|
|
65
|
+
# path, instead of as "command not found" halfway through a run.
|
|
66
|
+
AIUR_ENGINE_MODULES="identity oneshot dotenv run processes reap instance session control commands executor_commands report_commands lifecycle upgrade"
|
|
67
|
+
for __aiur_engine_module in $AIUR_ENGINE_MODULES; do
|
|
68
|
+
[ -f "$engine_dir/engine/$__aiur_engine_module.sh" ] ||
|
|
69
|
+
die "missing engine module: $engine_dir/engine/$__aiur_engine_module.sh"
|
|
70
|
+
source "$engine_dir/engine/$__aiur_engine_module.sh"
|
|
71
|
+
done
|
|
72
|
+
unset __aiur_engine_module
|
|
4009
73
|
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
74
|
+
usage() {
|
|
75
|
+
cat <<'EOF'
|
|
76
|
+
Usage: aiur [--interactive] [--no-dashboard] [--executor] [--pause] [--max-agents <n>] [--logs-root <path>] [--port <port>] [--host <host>] [config-path]
|
|
77
|
+
aiur run [--bg] [--no-dashboard] [--executor] [--debug] explicit launch form (foreground unless --bg)
|
|
78
|
+
aiur init [--force] scaffold .aiur/config (interactive setup wizard)
|
|
79
|
+
aiur login <harness> <name> [--dir <path>] sign in to a supported backend account
|
|
80
|
+
aiur accounts [<harness>] [--json] list registered accounts
|
|
81
|
+
aiur logout <harness> <name> [--purge] remove a backend account
|
|
82
|
+
aiur --bg [--no-dashboard] [--executor] [--debug] start detached; dashboard on unless suppressed
|
|
83
|
+
aiur stop stop the running session
|
|
84
|
+
aiur restart [--no-build] [run flags] stop, refresh the build, start again (detached)
|
|
85
|
+
aiur status show agent status
|
|
86
|
+
aiur agents show each agent's state + current activity
|
|
87
|
+
aiur commands [<decision-id>] [--filter all|open|blocking|resolved] [--blocking] [--ticket <id>] [--search <text>] [--cursor <cursor>] [--limit <n>] [--json]
|
|
88
|
+
aiur executor-answer <decision-id> --expected-version <n> (--option <id>|--custom-response <text>) --rationale <text> --idempotency-key <key> [--supersede] [--executor-id <id>]
|
|
89
|
+
aiur executor-escalate <decision-id> --expected-version <n> --reason <text> [--executor-id <id>]
|
|
90
|
+
aiur executor-moot <decision-id> --expected-version <n> --reason-class <class> [--reason <text>] [--executor-id <id>]
|
|
91
|
+
aiur units [--scope live|unfinished|all|none] [--condition active|alert|paused|queued|finished]... [--format auto|table|records] [--json]
|
|
92
|
+
aiur queue show [--queue NAME] [--json] read queue; add/remove/reorder/hold/release/recover/clear steer it
|
|
93
|
+
aiur build-orders [<root>] [--json] show the Build Order catalog or one root
|
|
94
|
+
aiur epic set <epic> <ids...> [--as <who>] [--source cli|backfill-agent] [--json]
|
|
95
|
+
aiur epic clear <ids...> [--as <who>] [--json]
|
|
96
|
+
aiur epic show [<ids...>] [--json]
|
|
97
|
+
aiur epic list [--json]
|
|
98
|
+
aiur analytics [--range run|full] [--since <ISO-8601>] [--until <ISO-8601>] [--build-order <id>] [--json]
|
|
99
|
+
aiur github-cost [--budget graphql|core|all] [--format auto|table|records] [--json] rank GitHub API spend by call site
|
|
100
|
+
aiur capabilities [--json] read-only instance capability report
|
|
101
|
+
aiur github-usage [--json] per-actor (daemon vs agent) GitHub usage and ceilings
|
|
102
|
+
aiur alerts [--needs-attention] [--limit <n> | --all] show structured alert feed
|
|
103
|
+
aiur watch [--full|--changes] [--interval <secs>] server-side status board
|
|
104
|
+
aiur listen [--topic <pattern> | --ticket <id>] stream events as JSON lines
|
|
105
|
+
aiur executor-listen [--topic <pattern>] deprecated alias for listen
|
|
106
|
+
aiur executor-wait [--timeout <seconds>] [--json] block until Executor work arrives
|
|
107
|
+
aiur executor-fast-forward <wake-id> [--as <id>] acknowledge an externally covered wake prefix
|
|
108
|
+
aiur executor-emit <topic> --payload <json> publish an Executor event
|
|
109
|
+
aiur executor-subscribe|executor-unsubscribe <pattern>
|
|
110
|
+
aiur executor-subscriptions list persistent Executor bindings
|
|
111
|
+
aiur workspace-recover <ticket-identifier> <generation> release a held workspace after verified provider exit
|
|
112
|
+
aiur executor-roster [--json] list Executor consumers with their liveness evidence
|
|
113
|
+
aiur executor-claim [--as <id>] claim the wake stream, or refuse and name the live owner
|
|
114
|
+
aiur executor-release [--as <id>] give up this consumer's claim
|
|
115
|
+
aiur executor-revoke <consumer-id> operator-only revoke of a live owner's claim
|
|
116
|
+
aiur set max-agents <n> change the concurrent-agent cap at runtime
|
|
117
|
+
aiur upgrade [--force] install the newer aiur-cli on your channel
|
|
118
|
+
aiur pause | resume flip the global pause switch (whole daemon)
|
|
119
|
+
aiur pause <ids|--all> | resume <ids|--all> per-agent pause/resume
|
|
120
|
+
aiur message <id> [--message-id ID] <text> send Executor text to a running agent
|
|
121
|
+
aiur --todo <ids...> [--only] queue tickets; optionally dequeue all other pending tickets
|
|
122
|
+
aiur findings [--unfiled] [--slugs] [--scope aiur|repo] inspect host-local findings
|
|
123
|
+
aiur findings --record <json> --repo <owner/repo> append one validated finding
|
|
124
|
+
aiur findings --digest [--scope aiur|repo] generate the promoted Markdown digest
|
|
125
|
+
aiur ask <title> [--body <text>|--body-file <path>] [--urgency low|normal|high] [--blocking]
|
|
126
|
+
aiur ask --done <id> [--note <text>] create or resolve an operator request
|
|
127
|
+
aiur asks [--open|--all] [--json] inspect current-repository operator requests
|
|
128
|
+
aiur doctor [--repair] check mise shims; repair only with consent
|
|
129
|
+
aiur cleanup-stale [--dry-run] list/reap stale manual-smoke leftovers
|
|
130
|
+
aiur --version
|
|
131
|
+
Bare aiur: start or attach to this directory's interactive session.
|
|
132
|
+
EOF
|
|
4013
133
|
}
|
|
4014
134
|
|
|
4015
135
|
# --- dispatch ----------------------------------------------------------------
|
|
4016
|
-
|
|
4017
136
|
dispatch_run() {
|
|
137
|
+
[ ! -f "$engine_dir/aiur-mise-doctor" ] || bash "$engine_dir/aiur-mise-doctor" --check || true
|
|
4018
138
|
local mode="foreground" arg
|
|
4019
139
|
local args=()
|
|
4020
140
|
|
|
@@ -4032,7 +152,6 @@ dispatch_run() {
|
|
|
4032
152
|
# `set -u` — happens for a bare `--bg` run. Guard the expansion.
|
|
4033
153
|
run_session "$mode" "${args[@]+"${args[@]}"}"
|
|
4034
154
|
}
|
|
4035
|
-
|
|
4036
155
|
aiur_engine_main() {
|
|
4037
156
|
local cmd="${1:-}"
|
|
4038
157
|
# Names the running subcommand in control-RPC diagnostics so a failure says
|
|
@@ -4058,6 +177,16 @@ aiur_engine_main() {
|
|
|
4058
177
|
init)
|
|
4059
178
|
run_init "$@"
|
|
4060
179
|
;;
|
|
180
|
+
login)
|
|
181
|
+
run_account_login "$@"
|
|
182
|
+
;;
|
|
183
|
+
accounts)
|
|
184
|
+
shift
|
|
185
|
+
cmd_accounts "$@"
|
|
186
|
+
;;
|
|
187
|
+
logout)
|
|
188
|
+
run_local_cli "$@"
|
|
189
|
+
;;
|
|
4061
190
|
findings)
|
|
4062
191
|
run_findings "$@"
|
|
4063
192
|
;;
|
|
@@ -4071,6 +200,7 @@ aiur_engine_main() {
|
|
|
4071
200
|
shift
|
|
4072
201
|
dispatch_run "$@"
|
|
4073
202
|
;;
|
|
203
|
+
doctor) shift; exec bash "$engine_dir/aiur-mise-doctor" "$@" ;;
|
|
4074
204
|
status)
|
|
4075
205
|
shift
|
|
4076
206
|
cmd_status "$@"
|
|
@@ -4103,6 +233,11 @@ aiur_engine_main() {
|
|
|
4103
233
|
shift
|
|
4104
234
|
cmd_units "$@"
|
|
4105
235
|
;;
|
|
236
|
+
queue)
|
|
237
|
+
shift
|
|
238
|
+
cmd_queue "$@"
|
|
239
|
+
;;
|
|
240
|
+
epic) shift; cmd_epic "$@" ;;
|
|
4106
241
|
build-orders)
|
|
4107
242
|
shift
|
|
4108
243
|
cmd_build_orders "$@"
|
|
@@ -4115,6 +250,8 @@ aiur_engine_main() {
|
|
|
4115
250
|
shift
|
|
4116
251
|
cmd_github_cost "$@"
|
|
4117
252
|
;;
|
|
253
|
+
capabilities)
|
|
254
|
+
shift; cmd_capabilities "$@" ;;
|
|
4118
255
|
github-usage)
|
|
4119
256
|
shift
|
|
4120
257
|
cmd_github_usage "$@"
|
|
@@ -4127,9 +264,9 @@ aiur_engine_main() {
|
|
|
4127
264
|
shift
|
|
4128
265
|
cmd_watch "$@"
|
|
4129
266
|
;;
|
|
4130
|
-
executor-listen)
|
|
267
|
+
listen|executor-listen)
|
|
4131
268
|
shift
|
|
4132
|
-
|
|
269
|
+
cmd_listen "$@"
|
|
4133
270
|
;;
|
|
4134
271
|
executor-wait)
|
|
4135
272
|
shift
|
|
@@ -4183,6 +320,10 @@ aiur_engine_main() {
|
|
|
4183
320
|
shift
|
|
4184
321
|
cmd_reset_budget "$@"
|
|
4185
322
|
;;
|
|
323
|
+
workspace-recover)
|
|
324
|
+
shift
|
|
325
|
+
cmd_workspace_recover "$@"
|
|
326
|
+
;;
|
|
4186
327
|
message)
|
|
4187
328
|
shift
|
|
4188
329
|
cmd_message "$@"
|
|
@@ -4218,7 +359,10 @@ aiur_engine_main() {
|
|
|
4218
359
|
;;
|
|
4219
360
|
esac
|
|
4220
361
|
}
|
|
4221
|
-
|
|
362
|
+
source "$(dirname "${BASH_SOURCE[0]}")/aiur-daemon-supervisor.sh"
|
|
363
|
+
source "$(dirname "${BASH_SOURCE[0]}")/aiur-queue.sh"
|
|
364
|
+
source "$(dirname "${BASH_SOURCE[0]}")/aiur-epic.sh"
|
|
365
|
+
source "$(dirname "${BASH_SOURCE[0]}")/aiur-capabilities.sh"
|
|
4222
366
|
if [ "${BASH_SOURCE[0]}" = "${0}" ]; then
|
|
4223
367
|
aiur_engine_main "$@"
|
|
4224
368
|
fi
|