agent-control-plane 0.3.0 → 0.6.0

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.
Files changed (106) hide show
  1. package/README.md +141 -28
  2. package/assets/workflow-catalog.json +1 -1
  3. package/bin/pr-risk.sh +22 -7
  4. package/bin/sync-pr-labels.sh +1 -1
  5. package/hooks/heartbeat-hooks.sh +125 -12
  6. package/hooks/issue-reconcile-hooks.sh +1 -1
  7. package/hooks/pr-reconcile-hooks.sh +1 -1
  8. package/npm/bin/agent-control-plane.js +257 -59
  9. package/package.json +39 -32
  10. package/tools/bin/debug-session.sh +106 -0
  11. package/tools/bin/flow-config-lib.sh +1203 -60
  12. package/tools/bin/flow-runtime-doctor-linux.sh +136 -0
  13. package/tools/bin/flow-runtime-doctor.sh +5 -1
  14. package/tools/bin/flow-shell-lib.sh +32 -0
  15. package/tools/bin/github-core-rate-limit-state.sh +77 -0
  16. package/tools/bin/github-write-outbox.sh +470 -0
  17. package/tools/bin/heartbeat-loop-scheduling-lib.sh +7 -7
  18. package/tools/bin/heartbeat-safe-auto.sh +42 -0
  19. package/tools/bin/install-project-launchd.sh +17 -2
  20. package/tools/bin/install-project-systemd.sh +255 -0
  21. package/tools/bin/project-init.sh +21 -1
  22. package/tools/bin/project-launchd-bootstrap.sh +5 -1
  23. package/tools/bin/project-runtimectl.sh +91 -2
  24. package/tools/bin/project-systemd-bootstrap.sh +74 -0
  25. package/tools/bin/scaffold-profile.sh +61 -3
  26. package/tools/bin/uninstall-project-systemd.sh +87 -0
  27. package/tools/dashboard/app.js +228 -6
  28. package/tools/dashboard/dashboard_snapshot.py +55 -0
  29. package/tools/dashboard/issue_queue_state.py +101 -0
  30. package/tools/dashboard/server.py +123 -1
  31. package/tools/dashboard/styles.css +526 -455
  32. package/tools/templates/pr-fix-template.md +3 -1
  33. package/tools/templates/pr-merge-repair-template.md +2 -1
  34. package/references/architecture.md +0 -217
  35. package/references/commands.md +0 -128
  36. package/references/control-plane-map.md +0 -124
  37. package/references/docs-map.md +0 -73
  38. package/references/release-checklist.md +0 -65
  39. package/references/repo-map.md +0 -36
  40. package/tools/bin/agent-cleanup-worktree +0 -247
  41. package/tools/bin/agent-github-update-labels +0 -71
  42. package/tools/bin/agent-init-worktree +0 -216
  43. package/tools/bin/agent-project-archive-run +0 -52
  44. package/tools/bin/agent-project-capture-worker +0 -46
  45. package/tools/bin/agent-project-catch-up-issue-pr-links +0 -118
  46. package/tools/bin/agent-project-catch-up-merged-prs +0 -194
  47. package/tools/bin/agent-project-catch-up-scheduled-issue-retries +0 -123
  48. package/tools/bin/agent-project-cleanup-session +0 -513
  49. package/tools/bin/agent-project-detached-launch +0 -127
  50. package/tools/bin/agent-project-heartbeat-loop +0 -1029
  51. package/tools/bin/agent-project-open-issue-worktree +0 -89
  52. package/tools/bin/agent-project-open-pr-worktree +0 -80
  53. package/tools/bin/agent-project-publish-issue-pr +0 -465
  54. package/tools/bin/agent-project-reconcile-issue-session +0 -1398
  55. package/tools/bin/agent-project-reconcile-pr-session +0 -1230
  56. package/tools/bin/agent-project-retry-state +0 -147
  57. package/tools/bin/agent-project-run-claude-session +0 -805
  58. package/tools/bin/agent-project-run-codex-resilient +0 -955
  59. package/tools/bin/agent-project-run-codex-session +0 -435
  60. package/tools/bin/agent-project-run-kilo-session +0 -369
  61. package/tools/bin/agent-project-run-ollama-session +0 -658
  62. package/tools/bin/agent-project-run-openclaw-session +0 -1309
  63. package/tools/bin/agent-project-run-opencode-session +0 -377
  64. package/tools/bin/agent-project-run-pi-session +0 -479
  65. package/tools/bin/agent-project-sync-anchor-repo +0 -139
  66. package/tools/bin/agent-project-worker-status +0 -188
  67. package/tools/bin/branch-verification-guard.sh +0 -364
  68. package/tools/bin/capture-worker.sh +0 -18
  69. package/tools/bin/cleanup-worktree.sh +0 -52
  70. package/tools/bin/codex-quota +0 -31
  71. package/tools/bin/create-follow-up-issue.sh +0 -114
  72. package/tools/bin/dashboard-launchd-bootstrap.sh +0 -50
  73. package/tools/bin/issue-publish-localization-guard.sh +0 -142
  74. package/tools/bin/issue-publish-scope-guard.sh +0 -242
  75. package/tools/bin/issue-requires-local-workspace-install.sh +0 -31
  76. package/tools/bin/issue-resource-class.sh +0 -12
  77. package/tools/bin/kick-scheduler.sh +0 -75
  78. package/tools/bin/label-follow-up-issues.sh +0 -14
  79. package/tools/bin/new-pr-worktree.sh +0 -50
  80. package/tools/bin/new-worktree.sh +0 -49
  81. package/tools/bin/pr-risk.sh +0 -12
  82. package/tools/bin/prepare-worktree.sh +0 -142
  83. package/tools/bin/provider-cooldown-state.sh +0 -204
  84. package/tools/bin/publish-issue-worker.sh +0 -31
  85. package/tools/bin/reconcile-bootstrap-lib.sh +0 -113
  86. package/tools/bin/reconcile-issue-worker.sh +0 -34
  87. package/tools/bin/reconcile-pr-worker.sh +0 -34
  88. package/tools/bin/record-verification.sh +0 -71
  89. package/tools/bin/render-flow-config.sh +0 -98
  90. package/tools/bin/resident-issue-controller-lib.sh +0 -448
  91. package/tools/bin/resident-issue-queue-status.py +0 -35
  92. package/tools/bin/retry-state.sh +0 -31
  93. package/tools/bin/reuse-issue-worktree.sh +0 -121
  94. package/tools/bin/run-codex-bypass.sh +0 -3
  95. package/tools/bin/run-codex-safe.sh +0 -3
  96. package/tools/bin/run-codex-task.sh +0 -280
  97. package/tools/bin/serve-dashboard.sh +0 -5
  98. package/tools/bin/split-retained-slice.sh +0 -124
  99. package/tools/bin/start-issue-worker.sh +0 -943
  100. package/tools/bin/start-pr-fix-worker.sh +0 -491
  101. package/tools/bin/start-pr-merge-repair-worker.sh +0 -8
  102. package/tools/bin/start-pr-review-worker.sh +0 -261
  103. package/tools/bin/start-resident-issue-loop.sh +0 -499
  104. package/tools/bin/update-github-labels.sh +0 -14
  105. package/tools/bin/worker-status.sh +0 -19
  106. package/tools/bin/workflow-catalog.sh +0 -77
@@ -1,955 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
-
4
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
- # shellcheck source=/dev/null
6
- source "${SCRIPT_DIR}/flow-config-lib.sh"
7
-
8
- usage() {
9
- cat <<'EOF'
10
- Usage:
11
- agent-project-run-codex-resilient --mode safe|bypass --worktree <path> --prompt-file <path> --output-file <path> --host-run-dir <path> --sandbox-run-dir <path> --codex-bin <path> [options]
12
-
13
- Run Codex with persisted thread recovery for quota/auth interruptions.
14
-
15
- Options:
16
- --safe-profile <name> Codex profile for safe mode
17
- --bypass-profile <name> Codex profile for bypass mode
18
- --max-resume-attempts <count> Maximum resume attempts after interruption
19
- --auth-refresh-timeout-seconds <secs> How long to wait for refreshed auth before failing
20
- --auth-refresh-poll-seconds <secs> Poll interval while waiting for refreshed auth
21
- --stall-seconds <secs> Fail if Codex stops producing output for too long
22
- --help Show this help
23
- EOF
24
- }
25
-
26
- mode=""
27
- worktree=""
28
- prompt_file=""
29
- output_file=""
30
- host_run_dir=""
31
- sandbox_run_dir=""
32
- safe_profile="default"
33
- bypass_profile="default"
34
- codex_bin=""
35
- max_resume_attempts="${ACP_CODEX_MAX_RESUME_ATTEMPTS:-${F_LOSNING_CODEX_MAX_RESUME_ATTEMPTS:-6}}"
36
- auth_refresh_timeout_seconds="${ACP_CODEX_AUTH_REFRESH_TIMEOUT_SECONDS:-${F_LOSNING_CODEX_AUTH_REFRESH_TIMEOUT_SECONDS:-900}}"
37
- auth_refresh_poll_seconds="${ACP_CODEX_AUTH_REFRESH_POLL_SECONDS:-${F_LOSNING_CODEX_AUTH_REFRESH_POLL_SECONDS:-10}}"
38
- max_quota_autoswitch_attempts="${ACP_CODEX_MAX_AUTOSWITCH_ATTEMPTS:-${F_LOSNING_CODEX_MAX_AUTOSWITCH_ATTEMPTS:-1}}"
39
- codex_progress_heartbeat_seconds="${ACP_CODEX_PROGRESS_HEARTBEAT_SECONDS:-${F_LOSNING_CODEX_PROGRESS_HEARTBEAT_SECONDS:-30}}"
40
- codex_stall_seconds="${ACP_CODEX_STALL_SECONDS:-${F_LOSNING_CODEX_STALL_SECONDS:-300}}"
41
- python_bin=""
42
-
43
- resolve_python_bin() {
44
- if command -v python3 >/dev/null 2>&1; then
45
- command -v python3
46
- return 0
47
- fi
48
- if [[ -x /opt/homebrew/bin/python3 ]]; then
49
- printf '%s\n' "/opt/homebrew/bin/python3"
50
- return 0
51
- fi
52
- if command -v python >/dev/null 2>&1; then
53
- command -v python
54
- return 0
55
- fi
56
- return 1
57
- }
58
-
59
- while [[ $# -gt 0 ]]; do
60
- case "$1" in
61
- --mode) mode="${2:-}"; shift 2 ;;
62
- --worktree) worktree="${2:-}"; shift 2 ;;
63
- --prompt-file) prompt_file="${2:-}"; shift 2 ;;
64
- --output-file) output_file="${2:-}"; shift 2 ;;
65
- --host-run-dir) host_run_dir="${2:-}"; shift 2 ;;
66
- --sandbox-run-dir) sandbox_run_dir="${2:-}"; shift 2 ;;
67
- --safe-profile) safe_profile="${2:-}"; shift 2 ;;
68
- --bypass-profile) bypass_profile="${2:-}"; shift 2 ;;
69
- --codex-bin) codex_bin="${2:-}"; shift 2 ;;
70
- --max-resume-attempts) max_resume_attempts="${2:-}"; shift 2 ;;
71
- --auth-refresh-timeout-seconds) auth_refresh_timeout_seconds="${2:-}"; shift 2 ;;
72
- --auth-refresh-poll-seconds) auth_refresh_poll_seconds="${2:-}"; shift 2 ;;
73
- --stall-seconds) codex_stall_seconds="${2:-}"; shift 2 ;;
74
- --help|-h) usage; exit 0 ;;
75
- *) echo "Unknown argument: $1" >&2; usage >&2; exit 1 ;;
76
- esac
77
- done
78
-
79
- if [[ -z "$mode" || -z "$worktree" || -z "$prompt_file" || -z "$output_file" || -z "$host_run_dir" || -z "$sandbox_run_dir" || -z "$codex_bin" ]]; then
80
- usage >&2
81
- exit 1
82
- fi
83
-
84
- case "$mode" in
85
- safe|bypass) ;;
86
- *)
87
- echo "--mode must be safe or bypass" >&2
88
- exit 1
89
- ;;
90
- esac
91
-
92
- case "$max_resume_attempts" in
93
- ''|*[!0-9]*) echo "--max-resume-attempts must be numeric" >&2; exit 1 ;;
94
- esac
95
- case "$auth_refresh_timeout_seconds" in
96
- ''|*[!0-9]*) echo "--auth-refresh-timeout-seconds must be numeric" >&2; exit 1 ;;
97
- esac
98
- case "$auth_refresh_poll_seconds" in
99
- ''|*[!0-9]*) echo "--auth-refresh-poll-seconds must be numeric" >&2; exit 1 ;;
100
- esac
101
- case "$max_quota_autoswitch_attempts" in
102
- ''|*[!0-9]*) echo "ACP_CODEX_MAX_AUTOSWITCH_ATTEMPTS must be numeric" >&2; exit 1 ;;
103
- esac
104
- case "$codex_progress_heartbeat_seconds" in
105
- ''|*[!0-9]*) echo "ACP_CODEX_PROGRESS_HEARTBEAT_SECONDS must be numeric" >&2; exit 1 ;;
106
- 0) echo "ACP_CODEX_PROGRESS_HEARTBEAT_SECONDS must be greater than zero" >&2; exit 1 ;;
107
- esac
108
- case "$codex_stall_seconds" in
109
- ''|*[!0-9]*) echo "ACP_CODEX_STALL_SECONDS must be numeric" >&2; exit 1 ;;
110
- esac
111
-
112
- python_bin="$(resolve_python_bin || true)"
113
- if [[ -z "$python_bin" || ! -x "$python_bin" ]]; then
114
- echo "unable to resolve a runnable python interpreter for codex supervision" >&2
115
- exit 1
116
- fi
117
-
118
- FLOW_SKILL_DIR="$(resolve_flow_skill_dir "${BASH_SOURCE[0]}")"
119
- state_file="${host_run_dir}/runner.env"
120
- auth_file="${HOME}/.codex/auth.json"
121
- shared_agent_home="${SHARED_AGENT_HOME:-$(resolve_shared_agent_home "${FLOW_SKILL_DIR}")}"
122
- quota_tool_bin="$(flow_resolve_codex_quota_bin "${FLOW_SKILL_DIR}")"
123
- quota_manager_script="$(flow_resolve_codex_quota_manager_script "${FLOW_SKILL_DIR}")"
124
- quota_autoswitch_enabled="${ACP_CODEX_QUOTA_AUTOSWITCH_ENABLED:-${F_LOSNING_CODEX_QUOTA_AUTOSWITCH_ENABLED:-1}}"
125
- quota_threshold="${ACP_CODEX_QUOTA_THRESHOLD:-${F_LOSNING_CODEX_QUOTA_THRESHOLD:-70}}"
126
- quota_weekly_threshold="${ACP_CODEX_QUOTA_WEEKLY_THRESHOLD:-${F_LOSNING_CODEX_QUOTA_WEEKLY_THRESHOLD:-90}}"
127
- quota_soft_threshold="${ACP_CODEX_QUOTA_SOFT_THRESHOLD:-${F_LOSNING_CODEX_QUOTA_SOFT_THRESHOLD:-55}}"
128
- quota_soft_worker_threshold="${ACP_CODEX_QUOTA_SOFT_WORKER_THRESHOLD:-${F_LOSNING_CODEX_QUOTA_SOFT_WORKER_THRESHOLD:-8}}"
129
- quota_emergency_threshold="${ACP_CODEX_QUOTA_EMERGENCY_THRESHOLD:-${F_LOSNING_CODEX_QUOTA_EMERGENCY_THRESHOLD:-65}}"
130
- quota_emergency_worker_threshold="${ACP_CODEX_QUOTA_EMERGENCY_WORKER_THRESHOLD:-${F_LOSNING_CODEX_QUOTA_EMERGENCY_WORKER_THRESHOLD:-12}}"
131
- quota_switch_cooldown_seconds="${ACP_CODEX_QUOTA_SWITCH_COOLDOWN_SECONDS:-${F_LOSNING_CODEX_QUOTA_SWITCH_COOLDOWN_SECONDS:-600}}"
132
- quota_timeout_seconds="${ACP_CODEX_QUOTA_TIMEOUT_SECONDS:-${F_LOSNING_CODEX_QUOTA_TIMEOUT_SECONDS:-45}}"
133
- quota_prefer_label="${ACP_CODEX_QUOTA_PREFER_LABEL:-${F_LOSNING_CODEX_QUOTA_PREFER_LABEL:-}}"
134
- quota_switch_state_file="${CODEX_QUOTA_MANAGER_SWITCH_STATE_FILE:-${XDG_CACHE_HOME:-$HOME/.cache}/codex-quota-manager/last-switch.env}"
135
- config_yaml="$(resolve_flow_config_yaml "${BASH_SOURCE[0]}")"
136
- issue_session_prefix="$(flow_resolve_issue_session_prefix "${config_yaml}")"
137
- pr_session_prefix="$(flow_resolve_pr_session_prefix "${config_yaml}")"
138
-
139
- thread_id=""
140
- attempt=0
141
- resume_count=0
142
- last_exit_code=""
143
- last_failure_reason=""
144
- last_trigger_reason=""
145
- auth_wait_started_at=""
146
- last_auth_fingerprint=""
147
- last_attempt_start_size=0
148
- last_attempt_start_quota_label=""
149
- last_quota_switch_status=""
150
- last_quota_next_retry_at=""
151
- last_quota_selected_label=""
152
- quota_autoswitch_attempt_count=0
153
- last_attempt_started_epoch=0
154
-
155
- mkdir -p "$host_run_dir"
156
- touch "$output_file"
157
-
158
- log_runner() {
159
- local message="${1:-}"
160
- [[ -n "$message" ]] || return 0
161
- printf '[%s] %s\n' "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" "$message" | tee -a "$output_file"
162
- }
163
-
164
- write_state() {
165
- local runner_state="${1:?runner state required}"
166
- local failure_reason="${2:-$last_failure_reason}"
167
- local updated_at
168
- local tmp_file="${state_file}.tmp.$$"
169
-
170
- updated_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
171
- {
172
- printf 'RUNNER_STATE=%q\n' "$runner_state"
173
- printf 'THREAD_ID=%q\n' "$thread_id"
174
- printf 'ATTEMPT=%s\n' "$attempt"
175
- printf 'RESUME_COUNT=%s\n' "$resume_count"
176
- printf 'LAST_EXIT_CODE=%q\n' "$last_exit_code"
177
- printf 'LAST_FAILURE_REASON=%q\n' "$failure_reason"
178
- printf 'LAST_TRIGGER_REASON=%q\n' "$last_trigger_reason"
179
- printf 'AUTH_WAIT_STARTED_AT=%q\n' "$auth_wait_started_at"
180
- printf 'LAST_AUTH_FINGERPRINT=%q\n' "$last_auth_fingerprint"
181
- printf 'UPDATED_AT=%q\n' "$updated_at"
182
- } >"$tmp_file"
183
- mv "$tmp_file" "$state_file"
184
- }
185
-
186
- run_codex_command() {
187
- # Nested workers must not inherit a parent thread id; the wrapper persists the child thread explicitly.
188
- env -u CODEX_THREAD_ID "$codex_bin" "$@"
189
- }
190
-
191
- codex_recovery_target() {
192
- if [[ -n "$thread_id" ]]; then
193
- printf 'thread %s' "$thread_id"
194
- return 0
195
- fi
196
- printf 'initial Codex exec'
197
- }
198
-
199
- run_with_timeout() {
200
- local timeout_seconds="${1:?timeout seconds required}"
201
- shift
202
-
203
- "$python_bin" - "$timeout_seconds" "$@" <<'PY'
204
- import os
205
- import signal
206
- import subprocess
207
- import sys
208
-
209
- timeout_seconds = float(sys.argv[1])
210
- argv = sys.argv[2:]
211
-
212
- if not argv:
213
- sys.exit(64)
214
-
215
- proc = subprocess.Popen(argv, start_new_session=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
216
-
217
- try:
218
- stdout, stderr = proc.communicate(timeout=timeout_seconds)
219
- except subprocess.TimeoutExpired:
220
- try:
221
- os.killpg(proc.pid, signal.SIGTERM)
222
- except ProcessLookupError:
223
- pass
224
- try:
225
- stdout, stderr = proc.communicate(timeout=2)
226
- except subprocess.TimeoutExpired:
227
- try:
228
- os.killpg(proc.pid, signal.SIGKILL)
229
- except ProcessLookupError:
230
- pass
231
- stdout, stderr = proc.communicate()
232
- if stdout:
233
- sys.stdout.buffer.write(stdout)
234
- if stderr:
235
- sys.stderr.buffer.write(stderr)
236
- sys.exit(124)
237
-
238
- if stdout:
239
- sys.stdout.buffer.write(stdout)
240
- if stderr:
241
- sys.stderr.buffer.write(stderr)
242
- sys.exit(proc.returncode)
243
- PY
244
- }
245
-
246
- stat_file_size() {
247
- local path="${1:?path required}"
248
- local value=""
249
-
250
- value="$(stat -f %z "$path" 2>/dev/null || true)"
251
- if [[ "$value" =~ ^[0-9]+$ ]]; then
252
- printf '%s\n' "$value"
253
- return 0
254
- fi
255
-
256
- value="$(stat -c %s "$path" 2>/dev/null || true)"
257
- if [[ "$value" =~ ^[0-9]+$ ]]; then
258
- printf '%s\n' "$value"
259
- return 0
260
- fi
261
-
262
- "$python_bin" - "$path" <<'PY'
263
- import os
264
- import sys
265
-
266
- try:
267
- print(os.path.getsize(sys.argv[1]))
268
- except OSError:
269
- print("0")
270
- PY
271
- }
272
-
273
- stat_file_mtime() {
274
- local path="${1:?path required}"
275
- local value=""
276
-
277
- value="$(stat -f %m "$path" 2>/dev/null || true)"
278
- if [[ "$value" =~ ^[0-9]+$ ]]; then
279
- printf '%s\n' "$value"
280
- return 0
281
- fi
282
-
283
- value="$(stat -c %Y "$path" 2>/dev/null || true)"
284
- if [[ "$value" =~ ^[0-9]+$ ]]; then
285
- printf '%s\n' "$value"
286
- return 0
287
- fi
288
-
289
- "$python_bin" - "$path" <<'PY'
290
- import os
291
- import sys
292
-
293
- try:
294
- print(int(os.path.getmtime(sys.argv[1])))
295
- except OSError:
296
- print("0")
297
- PY
298
- }
299
-
300
- auth_fingerprint() {
301
- if [[ ! -f "$auth_file" ]]; then
302
- printf 'missing\n'
303
- return 0
304
- fi
305
-
306
- local mtime size sha
307
- mtime="$(stat_file_mtime "$auth_file" 2>/dev/null || printf '0')"
308
- size="$(stat_file_size "$auth_file" 2>/dev/null || printf '0')"
309
- sha="$(shasum -a 256 "$auth_file" | awk '{print $1}')"
310
- printf '%s:%s:%s\n' "$mtime" "$size" "$sha"
311
- }
312
-
313
- quota_active_label() {
314
- if [[ ! -x "$quota_tool_bin" ]] || ! command -v jq >/dev/null 2>&1; then
315
- printf '\n'
316
- return 0
317
- fi
318
-
319
- "$quota_tool_bin" codex list --json 2>/dev/null \
320
- | jq -r '
321
- .activeInfo.trackedLabel
322
- // .activeInfo.activeLabel
323
- // ([.accounts[]? | select(.isActive == true or .isNativeActive == true)][0].label)
324
- // empty
325
- ' 2>/dev/null \
326
- || printf '\n'
327
- }
328
-
329
- quota_switch_signature() {
330
- if [[ ! -f "$quota_switch_state_file" ]]; then
331
- printf 'missing\n'
332
- return 0
333
- fi
334
-
335
- local mtime size sha
336
- mtime="$(stat_file_mtime "$quota_switch_state_file" 2>/dev/null || printf '0')"
337
- size="$(stat_file_size "$quota_switch_state_file" 2>/dev/null || printf '0')"
338
- sha="$(shasum -a 256 "$quota_switch_state_file" | awk '{print $1}')"
339
- printf '%s:%s:%s\n' "$mtime" "$size" "$sha"
340
- }
341
-
342
- running_workers() {
343
- if ! command -v tmux >/dev/null 2>&1; then
344
- printf '0\n'
345
- return 0
346
- fi
347
-
348
- { tmux list-sessions -F '#S' 2>/dev/null || true; } \
349
- | awk -v issue_prefix="$issue_session_prefix" -v pr_prefix="$pr_session_prefix" '
350
- index($0, issue_prefix) == 1 || index($0, pr_prefix) == 1 { count++ }
351
- END { print count + 0 }
352
- '
353
- }
354
-
355
- extract_kv_value() {
356
- local key="${1:?key required}"
357
- local payload="${2:-}"
358
- sed -nE "s/^${key}=(.*)$/\\1/p" <<<"$payload" | tail -n 1
359
- }
360
-
361
- run_quota_autoswitch() {
362
- local quota_output quota_status shell_flags_before_quota_exec
363
- local -a quota_cmd
364
- local worker_count
365
-
366
- quota_autoswitch_unavailable_reason() {
367
- if [[ "$quota_autoswitch_enabled" == "0" ]]; then
368
- printf 'disabled\n'
369
- return 0
370
- fi
371
- if [[ ! -x "$quota_manager_script" ]]; then
372
- printf 'missing-script\n'
373
- return 0
374
- fi
375
- if [[ ! -x "$quota_tool_bin" ]]; then
376
- printf 'missing-codex-quota\n'
377
- return 0
378
- fi
379
- if ! command -v jq >/dev/null 2>&1; then
380
- printf 'missing-jq\n'
381
- return 0
382
- fi
383
- printf 'ok\n'
384
- }
385
-
386
- local unavailable_reason=""
387
- last_quota_next_retry_at=""
388
- last_quota_selected_label=""
389
- if [[ "$quota_autoswitch_enabled" == "0" ]]; then
390
- log_runner "quota auto-switch disabled; waiting for external Codex auth refresh"
391
- printf 'CODEX_QUOTA_AUTOSWITCH_ENABLED=0\n' | tee -a "$output_file"
392
- last_quota_switch_status="disabled"
393
- return 1
394
- fi
395
-
396
- unavailable_reason="$(quota_autoswitch_unavailable_reason)"
397
- if [[ "$unavailable_reason" != "ok" ]]; then
398
- log_runner "quota auto-switch unavailable (${unavailable_reason}); waiting for external Codex auth refresh"
399
- printf 'CODEX_QUOTA_MANAGER_UNAVAILABLE=yes\n' | tee -a "$output_file"
400
- printf 'CODEX_QUOTA_MANAGER_REASON=%s\n' "$unavailable_reason" | tee -a "$output_file"
401
- last_quota_switch_status="unavailable"
402
- return 1
403
- fi
404
-
405
- quota_autoswitch_attempt_count=$((quota_autoswitch_attempt_count + 1))
406
- worker_count="$(running_workers)"
407
- quota_cmd=(
408
- env
409
- "CODEX_QUOTA_BIN=${quota_tool_bin}"
410
- bash "$quota_manager_script"
411
- --trigger-reason "$last_failure_reason"
412
- --current-label "${last_attempt_start_quota_label}"
413
- --five-hour-threshold "$quota_threshold"
414
- --weekly-threshold "$quota_weekly_threshold"
415
- --running-workers "$worker_count"
416
- )
417
- if [[ -n "$quota_prefer_label" ]]; then
418
- quota_cmd+=(--prefer-label "$quota_prefer_label")
419
- fi
420
-
421
- log_runner "${last_failure_reason} detected; attempting failure-driven Codex account switch"
422
- shell_flags_before_quota_exec="$-"
423
- set +e
424
- quota_output="$(run_with_timeout "$quota_timeout_seconds" "${quota_cmd[@]}" 2>&1)"
425
- quota_status=$?
426
- case "$shell_flags_before_quota_exec" in
427
- *e*) set -e ;;
428
- *) set +e ;;
429
- esac
430
-
431
- if [[ "$quota_status" == "0" ]]; then
432
- last_quota_switch_status="$(extract_kv_value "SWITCH_DECISION" "$quota_output")"
433
- last_quota_next_retry_at="$(extract_kv_value "NEXT_RETRY_AT" "$quota_output")"
434
- last_quota_selected_label="$(extract_kv_value "SELECTED_LABEL" "$quota_output")"
435
- [[ -n "$quota_output" ]] && printf '%s\n' "$quota_output" | tee -a "$output_file"
436
- case "$last_quota_switch_status" in
437
- switched|current-ok)
438
- return 0
439
- ;;
440
- deferred)
441
- return 10
442
- ;;
443
- *)
444
- last_quota_switch_status="failed"
445
- return 1
446
- ;;
447
- esac
448
- fi
449
-
450
- last_quota_next_retry_at="$(extract_kv_value "NEXT_RETRY_AT" "${quota_output:-}")"
451
- last_quota_selected_label="$(extract_kv_value "SELECTED_LABEL" "${quota_output:-}")"
452
- [[ -n "${quota_output:-}" ]] && printf '%s\n' "$quota_output" | tee -a "$output_file"
453
- if [[ "$quota_status" == "10" ]]; then
454
- last_quota_switch_status="deferred"
455
- log_runner "no eligible Codex account is ready yet; waiting for the next reset window"
456
- return 10
457
- fi
458
- last_quota_switch_status="failed"
459
- if [[ "$quota_status" == "124" ]]; then
460
- log_runner "quota auto-switch timed out after ${quota_timeout_seconds}s"
461
- else
462
- log_runner "quota auto-switch exited with status ${quota_status}"
463
- fi
464
- return "$quota_status"
465
- }
466
-
467
- new_output_since() {
468
- local start_size="${1:?start size required}"
469
- local file_size
470
- file_size="$(stat_file_size "$output_file" 2>/dev/null || printf '0')"
471
- if (( file_size <= start_size )); then
472
- return 0
473
- fi
474
- tail -c "+$((start_size + 1))" "$output_file"
475
- }
476
-
477
- update_thread_id_from_output() {
478
- local start_size="${1:?start size required}"
479
- local new_thread_id
480
-
481
- new_thread_id="$(new_output_since "$start_size" | extract_thread_id || true)"
482
- if [[ -n "$new_thread_id" ]]; then
483
- thread_id="$new_thread_id"
484
- fi
485
- }
486
-
487
- extract_thread_id_from_line() {
488
- local line="${1:-}"
489
- sed -nE 's/.*"type"[[:space:]]*:[[:space:]]*"thread\.started".*"thread_id"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/p' <<<"$line"
490
- }
491
-
492
- persist_thread_id_from_line() {
493
- local line="${1:-}"
494
- local new_thread_id=""
495
-
496
- new_thread_id="$(extract_thread_id_from_line "$line")"
497
- if [[ -n "$new_thread_id" && "$new_thread_id" != "$thread_id" ]]; then
498
- thread_id="$new_thread_id"
499
- write_state "running" ""
500
- fi
501
- }
502
-
503
- terminate_codex_producer_tree() {
504
- local pid="${1:?pid required}"
505
- local deadline=""
506
-
507
- if ! kill -0 "$pid" 2>/dev/null; then
508
- return 0
509
- fi
510
-
511
- pkill -TERM -P "$pid" 2>/dev/null || true
512
- kill "$pid" 2>/dev/null || true
513
-
514
- deadline=$(( $(date +%s) + 2 ))
515
- while kill -0 "$pid" 2>/dev/null; do
516
- if (( $(date +%s) >= deadline )); then
517
- break
518
- fi
519
- sleep 0.1
520
- done
521
-
522
- if kill -0 "$pid" 2>/dev/null; then
523
- pkill -KILL -P "$pid" 2>/dev/null || true
524
- kill -9 "$pid" 2>/dev/null || true
525
- fi
526
- }
527
-
528
- stream_codex_exec() {
529
- local phase="${1:?phase required}"
530
- local stream_fifo=""
531
- local producer_pid=""
532
- local heartbeat_pid=""
533
- local progress_file=""
534
- local line=""
535
-
536
- last_attempt_start_size="$(stat_file_size "$output_file" 2>/dev/null || printf '0')"
537
- last_attempt_started_epoch="$(date +%s)"
538
- progress_file="${host_run_dir}/.codex-progress.$$"
539
- rm -f "$progress_file"
540
- stream_fifo="$(mktemp -u "${TMPDIR:-/tmp}/codex-stream.XXXXXX")"
541
- mkfifo "$stream_fifo"
542
-
543
- case "$phase" in
544
- initial)
545
- (
546
- case "$mode" in
547
- safe)
548
- run_codex_command exec --json --profile "$safe_profile" --full-auto <"$prompt_file"
549
- ;;
550
- bypass)
551
- run_codex_command exec --json --profile "$bypass_profile" --dangerously-bypass-approvals-and-sandbox <"$prompt_file"
552
- ;;
553
- esac
554
- ) >"$stream_fifo" 2>&1 &
555
- ;;
556
- resume)
557
- (
558
- case "$mode" in
559
- safe)
560
- resume_prompt | run_codex_command exec resume --json --full-auto "$thread_id" -
561
- ;;
562
- bypass)
563
- resume_prompt | run_codex_command exec resume --json --dangerously-bypass-approvals-and-sandbox "$thread_id" -
564
- ;;
565
- esac
566
- ) >"$stream_fifo" 2>&1 &
567
- ;;
568
- *)
569
- rm -f "$stream_fifo"
570
- echo "unknown codex exec phase: $phase" >&2
571
- exit 1
572
- ;;
573
- esac
574
-
575
- producer_pid="$!"
576
- (
577
- local now elapsed last_progress_epoch idle_for
578
- while kill -0 "$producer_pid" 2>/dev/null; do
579
- sleep "$codex_progress_heartbeat_seconds"
580
- if ! kill -0 "$producer_pid" 2>/dev/null; then
581
- break
582
- fi
583
- now="$(date +%s)"
584
- elapsed=$((now - last_attempt_started_epoch))
585
- if (( codex_stall_seconds > 0 )); then
586
- if [[ ! -f "$progress_file" ]]; then
587
- if (( elapsed >= codex_stall_seconds )); then
588
- write_state "running" ""
589
- log_runner "stale-run no-codex-output-before-stall-threshold elapsed=${elapsed}s"
590
- terminate_codex_producer_tree "$producer_pid"
591
- break
592
- fi
593
- else
594
- last_progress_epoch="$(stat_file_mtime "$progress_file" 2>/dev/null || printf '0')"
595
- if [[ -n "$last_progress_epoch" && "$last_progress_epoch" != "0" ]]; then
596
- idle_for=$((now - last_progress_epoch))
597
- if (( idle_for >= codex_stall_seconds )); then
598
- write_state "running" ""
599
- log_runner "stale-run no-codex-progress-before-stall-threshold elapsed=${elapsed}s idle=${idle_for}s"
600
- terminate_codex_producer_tree "$producer_pid"
601
- break
602
- fi
603
- fi
604
- fi
605
- fi
606
- write_state "running" ""
607
- log_runner "heartbeat waiting-for-codex-output elapsed=${elapsed}s"
608
- done
609
- ) &
610
- heartbeat_pid="$!"
611
-
612
- while IFS= read -r line || [[ -n "$line" ]]; do
613
- printf '%s\n' "$line" | tee -a "$output_file"
614
- touch "$progress_file" 2>/dev/null || true
615
- persist_thread_id_from_line "$line"
616
- done <"$stream_fifo"
617
-
618
- if [[ -n "$heartbeat_pid" ]] && kill -0 "$heartbeat_pid" 2>/dev/null; then
619
- kill "$heartbeat_pid" 2>/dev/null || true
620
- wait "$heartbeat_pid" 2>/dev/null || true
621
- fi
622
-
623
- rm -f "$stream_fifo"
624
- rm -f "$progress_file"
625
-
626
- if wait "$producer_pid"; then
627
- last_exit_code="0"
628
- else
629
- last_exit_code="$?"
630
- fi
631
-
632
- update_thread_id_from_output "$last_attempt_start_size"
633
- }
634
-
635
- extract_thread_id() {
636
- "$python_bin" -c '
637
- import json
638
- import sys
639
-
640
- thread_id = ""
641
- for raw in sys.stdin:
642
- line = raw.strip()
643
- if not line.startswith("{"):
644
- continue
645
- try:
646
- payload = json.loads(line)
647
- except Exception:
648
- continue
649
- if payload.get("type") == "thread.started" and payload.get("thread_id"):
650
- thread_id = str(payload["thread_id"])
651
-
652
- if thread_id:
653
- sys.stdout.write(thread_id)
654
- '
655
- }
656
-
657
- classify_failure_reason() {
658
- local chunk="${1:-}"
659
- local recent_chunk
660
-
661
- recent_chunk="$(tail -n 120 <<<"$chunk")"
662
-
663
- if grep -Eiq 'stale-run no-codex-output-before-stall-threshold|no-codex-output-before-stall-threshold' <<<"$recent_chunk"; then
664
- printf 'no-codex-output-before-stall-threshold\n'
665
- return 0
666
- fi
667
-
668
- if grep -Eiq 'stale-run no-codex-progress-before-stall-threshold|no-codex-progress-before-stall-threshold' <<<"$recent_chunk"; then
669
- printf 'no-codex-progress-before-stall-threshold\n'
670
- return 0
671
- fi
672
-
673
- if grep -Eiq "You've hit your usage limit|You have reached your Codex usage limits|visit https://chatgpt.com/codex/settings/usage|Upgrade to Pro|rate limit exceeded|quota exceeded|usage cap (reached|exceeded)|usage quota (reached|exceeded)" <<<"$recent_chunk"; then
674
- printf 'usage-limit\n'
675
- return 0
676
- fi
677
-
678
- if grep -Eiq "(HTTP[^0-9]*)?401([^0-9]|$)|unauthorized|invalid credentials|invalid api key|authentication failed with status 401|received 401" <<<"$recent_chunk"; then
679
- printf 'auth-401\n'
680
- return 0
681
- fi
682
-
683
- if grep -Eiq "account (is )?(banned|suspended|disabled)|access revoked|account revoked|forbidden due to policy|account blocked|policy violation" <<<"$recent_chunk"; then
684
- printf 'account-banned\n'
685
- return 0
686
- fi
687
-
688
- if grep -Eiq "Authentication required|Please log in|Please login|Please authenticate|login required|run codex login|codex login required|logged out|not logged in|expired session|session expired|token expired|reauthenticate|unauthenticated|auth(entication)? failed|credentials expired" <<<"$recent_chunk"; then
689
- printf 'auth-failure\n'
690
- return 0
691
- fi
692
-
693
- if [[ -n "$last_exit_code" && "$last_exit_code" != "0" ]]; then
694
- printf 'worker-exit-failed\n'
695
- fi
696
- }
697
-
698
- failure_chunk_indicates_startup_stall() {
699
- local chunk="${1:-}"
700
- local recent_chunk
701
-
702
- recent_chunk="$(tail -n 120 <<<"$chunk")"
703
- grep -q '"type":"thread.started"' <<<"$recent_chunk" || return 1
704
- grep -q '"type":"turn.started"' <<<"$recent_chunk" || return 1
705
- if grep -Eq '"type":"item\.(started|completed)"' <<<"$recent_chunk"; then
706
- return 1
707
- fi
708
- if grep -q '"type":"turn.completed"' <<<"$recent_chunk"; then
709
- return 1
710
- fi
711
- return 0
712
- }
713
-
714
- resume_prompt() {
715
- cat <<EOF
716
- The previous Codex exec turn in this same thread was interrupted because the host refreshed Codex authentication after a quota or auth failure.
717
-
718
- Continue the same task from the next unfinished step only. Do not restart completed work unless you need to verify or repair it.
719
-
720
- If you need to reorient, inspect the current git status plus the existing run artifacts in:
721
- - Host run dir: ${host_run_dir}
722
- - Sandbox run dir: ${sandbox_run_dir}
723
- EOF
724
- }
725
-
726
- codex_login_healthy() {
727
- run_codex_command login status >/dev/null 2>&1
728
- }
729
-
730
- wait_for_auth_refresh() {
731
- local baseline_fingerprint="${1:?baseline fingerprint required}"
732
- local trigger_reason="${2:?trigger reason required}"
733
- local baseline_quota_label="${3:-}"
734
- local baseline_switch_signature="${4:-}"
735
- local deadline now current_fingerprint current_quota_label current_switch_signature
736
- local sleep_seconds
737
- local recovery_target
738
-
739
- recovery_target="$(codex_recovery_target)"
740
- auth_wait_started_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
741
- last_trigger_reason="$trigger_reason"
742
- write_state "waiting-auth-refresh" "$trigger_reason"
743
-
744
- deadline=$(( $(date +%s) + auth_refresh_timeout_seconds ))
745
- while :; do
746
- current_fingerprint="$(auth_fingerprint)"
747
- last_auth_fingerprint="$current_fingerprint"
748
- case "$trigger_reason" in
749
- usage-limit|auth-401|account-banned)
750
- current_quota_label="$(quota_active_label)"
751
- current_switch_signature="$(quota_switch_signature)"
752
- if codex_login_healthy; then
753
- if [[ "$current_fingerprint" != "$baseline_fingerprint" ]]; then
754
- log_runner "detected refreshed Codex auth after quota interruption; resuming ${recovery_target}"
755
- auth_wait_started_at=""
756
- write_state "running" ""
757
- return 0
758
- fi
759
-
760
- if [[ -n "$baseline_quota_label" && -n "$current_quota_label" && "$current_quota_label" != "$baseline_quota_label" ]]; then
761
- log_runner "detected rotated Codex quota account (${baseline_quota_label} -> ${current_quota_label}); resuming ${recovery_target}"
762
- auth_wait_started_at=""
763
- write_state "running" ""
764
- return 0
765
- fi
766
-
767
- if [[ -n "$baseline_switch_signature" && -n "$current_switch_signature" && "$current_switch_signature" != "$baseline_switch_signature" ]]; then
768
- log_runner "detected quota switch state refresh; resuming ${recovery_target}"
769
- auth_wait_started_at=""
770
- write_state "running" ""
771
- return 0
772
- fi
773
-
774
- if [[ "$last_quota_switch_status" == "switched" && -n "$current_quota_label" ]]; then
775
- log_runner "quota manager reports healthy Codex account ${current_quota_label}; resuming ${recovery_target}"
776
- auth_wait_started_at=""
777
- write_state "running" ""
778
- return 0
779
- fi
780
- fi
781
-
782
- ;;
783
- *)
784
- if codex_login_healthy; then
785
- if [[ "$current_fingerprint" != "$baseline_fingerprint" ]]; then
786
- log_runner "detected refreshed Codex auth; resuming ${recovery_target}"
787
- else
788
- log_runner "Codex auth is healthy again; resuming ${recovery_target}"
789
- fi
790
- auth_wait_started_at=""
791
- write_state "running" ""
792
- return 0
793
- fi
794
- ;;
795
- esac
796
-
797
- now="$(date +%s)"
798
- if (( now >= deadline )); then
799
- last_failure_reason="auth-refresh-timeout"
800
- write_state "failed" "$last_failure_reason"
801
- return 1
802
- fi
803
-
804
- sleep_seconds="$auth_refresh_poll_seconds"
805
- if (( sleep_seconds > deadline - now )); then
806
- sleep_seconds=$(( deadline - now ))
807
- fi
808
-
809
- if (( sleep_seconds < 1 )); then
810
- sleep_seconds=1
811
- fi
812
- sleep "$sleep_seconds"
813
- done
814
- }
815
-
816
- run_initial_exec() {
817
- stream_codex_exec initial
818
- }
819
-
820
- run_resume_exec() {
821
- stream_codex_exec resume
822
- }
823
-
824
- attempt_run() {
825
- local reason auth_before_switch quota_label_before_switch quota_switch_signature_before_switch quota_switch_result shell_flags_before_quota_switch failure_chunk startup_stall
826
-
827
- attempt=$((attempt + 1))
828
- last_quota_switch_status=""
829
- last_attempt_start_quota_label="$(quota_active_label)"
830
- write_state "running" ""
831
-
832
- if [[ -z "$thread_id" ]]; then
833
- log_runner "starting Codex exec attempt ${attempt}"
834
- run_initial_exec
835
- else
836
- log_runner "resuming Codex thread ${thread_id} (resume ${resume_count}/${max_resume_attempts})"
837
- run_resume_exec
838
- fi
839
-
840
- if [[ "${last_exit_code}" == "0" ]]; then
841
- last_failure_reason=""
842
- write_state "succeeded" ""
843
- return 0
844
- fi
845
-
846
- failure_chunk="$(new_output_since "$last_attempt_start_size")"
847
- reason="$(classify_failure_reason "$failure_chunk")"
848
- last_failure_reason="${reason:-worker-exit-failed}"
849
- startup_stall="no"
850
- if [[ "$last_failure_reason" == "no-codex-output-before-stall-threshold" || "$last_failure_reason" == "no-codex-progress-before-stall-threshold" ]]; then
851
- if failure_chunk_indicates_startup_stall "$failure_chunk"; then
852
- startup_stall="yes"
853
- fi
854
- fi
855
-
856
- case "$last_failure_reason" in
857
- usage-limit|auth-failure|auth-401|account-banned)
858
- if (( resume_count >= max_resume_attempts )); then
859
- last_failure_reason="resume-attempts-exhausted"
860
- write_state "failed" "$last_failure_reason"
861
- return 1
862
- fi
863
-
864
- auth_before_switch="$(auth_fingerprint)"
865
- quota_label_before_switch="$last_attempt_start_quota_label"
866
- quota_switch_signature_before_switch="$(quota_switch_signature)"
867
- last_auth_fingerprint="$auth_before_switch"
868
- if [[ "$last_failure_reason" == "usage-limit" || "$last_failure_reason" == "auth-401" || "$last_failure_reason" == "account-banned" ]]; then
869
- if (( quota_autoswitch_attempt_count >= max_quota_autoswitch_attempts )); then
870
- log_runner "automatic Codex quota switching already ran ${quota_autoswitch_attempt_count} time(s) in this worker; refusing another rotation"
871
- last_failure_reason="quota-switch-attempt-limit"
872
- write_state "failed" "$last_failure_reason"
873
- return 1
874
- fi
875
- write_state "switching-account" "$last_failure_reason"
876
- shell_flags_before_quota_switch="$-"
877
- set +e
878
- run_quota_autoswitch
879
- quota_switch_result=$?
880
- case "$shell_flags_before_quota_switch" in
881
- *e*) set -e ;;
882
- *) set +e ;;
883
- esac
884
- if [[ "$quota_switch_result" == "10" ]]; then
885
- log_runner "quota manager deferred rotation until ${last_quota_next_retry_at:-unknown}; automatic timed re-tries are disabled for safety"
886
- last_failure_reason="quota-switch-deferred"
887
- write_state "failed" "$last_failure_reason"
888
- return 1
889
- fi
890
- fi
891
-
892
- if ! wait_for_auth_refresh "$auth_before_switch" "$last_failure_reason" "$quota_label_before_switch" "$quota_switch_signature_before_switch"; then
893
- return 1
894
- fi
895
-
896
- resume_count=$((resume_count + 1))
897
- return 2
898
- ;;
899
- no-codex-output-before-stall-threshold|no-codex-progress-before-stall-threshold)
900
- if [[ "$startup_stall" == "yes" && $quota_autoswitch_attempt_count -lt $max_quota_autoswitch_attempts ]]; then
901
- auth_before_switch="$(auth_fingerprint)"
902
- quota_label_before_switch="$last_attempt_start_quota_label"
903
- quota_switch_signature_before_switch="$(quota_switch_signature)"
904
- last_auth_fingerprint="$auth_before_switch"
905
- write_state "switching-account" "$last_failure_reason"
906
- log_runner "startup-stall detected before first Codex tool activity; attempting Codex account rotation"
907
- shell_flags_before_quota_switch="$-"
908
- set +e
909
- run_quota_autoswitch
910
- quota_switch_result=$?
911
- case "$shell_flags_before_quota_switch" in
912
- *e*) set -e ;;
913
- *) set +e ;;
914
- esac
915
- if [[ "$quota_switch_result" == "0" ]]; then
916
- thread_id=""
917
- auth_wait_started_at=""
918
- write_state "running" ""
919
- return 2
920
- fi
921
- if [[ "$quota_switch_result" == "10" ]]; then
922
- log_runner "startup-stall rotation deferred until ${last_quota_next_retry_at:-unknown}"
923
- last_failure_reason="quota-switch-deferred"
924
- write_state "failed" "$last_failure_reason"
925
- return 1
926
- fi
927
- fi
928
- write_state "failed" "$last_failure_reason"
929
- return 1
930
- ;;
931
- *)
932
- write_state "failed" "$last_failure_reason"
933
- return 1
934
- ;;
935
- esac
936
- }
937
-
938
- write_state "running" ""
939
-
940
- while :; do
941
- set +e
942
- attempt_run
943
- attempt_status=$?
944
- set -e
945
-
946
- if [[ "$attempt_status" == "0" ]]; then
947
- exit 0
948
- fi
949
-
950
- if [[ "$attempt_status" == "2" ]]; then
951
- continue
952
- fi
953
-
954
- exit "${last_exit_code:-1}"
955
- done