agent-control-plane 0.2.0 → 0.3.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 (33) hide show
  1. package/npm/bin/agent-control-plane.js +39 -2
  2. package/package.json +6 -3
  3. package/tools/bin/agent-project-catch-up-merged-prs +1 -0
  4. package/tools/bin/agent-project-cleanup-session +49 -5
  5. package/tools/bin/agent-project-heartbeat-loop +119 -1471
  6. package/tools/bin/agent-project-reconcile-issue-session +66 -105
  7. package/tools/bin/agent-project-reconcile-pr-session +76 -111
  8. package/tools/bin/agent-project-run-claude-session +10 -0
  9. package/tools/bin/agent-project-run-codex-resilient +86 -9
  10. package/tools/bin/agent-project-run-codex-session +16 -5
  11. package/tools/bin/agent-project-run-kilo-session +10 -0
  12. package/tools/bin/agent-project-run-openclaw-session +10 -0
  13. package/tools/bin/agent-project-run-opencode-session +10 -0
  14. package/tools/bin/agent-project-worker-status +10 -7
  15. package/tools/bin/cleanup-worktree.sh +6 -1
  16. package/tools/bin/flow-config-lib.sh +80 -0
  17. package/tools/bin/flow-resident-worker-lib.sh +119 -1
  18. package/tools/bin/flow-shell-lib.sh +24 -0
  19. package/tools/bin/heartbeat-loop-cache-lib.sh +164 -0
  20. package/tools/bin/heartbeat-loop-counting-lib.sh +306 -0
  21. package/tools/bin/heartbeat-loop-pr-strategy-lib.sh +199 -0
  22. package/tools/bin/heartbeat-loop-scheduling-lib.sh +506 -0
  23. package/tools/bin/heartbeat-loop-worker-lib.sh +319 -0
  24. package/tools/bin/heartbeat-recovery-preflight.sh +12 -1
  25. package/tools/bin/heartbeat-safe-auto.sh +14 -3
  26. package/tools/bin/project-launchd-bootstrap.sh +11 -8
  27. package/tools/bin/reconcile-bootstrap-lib.sh +113 -0
  28. package/tools/bin/resident-issue-controller-lib.sh +448 -0
  29. package/tools/bin/resident-issue-queue-status.py +35 -0
  30. package/tools/bin/start-resident-issue-loop.sh +26 -437
  31. package/tools/dashboard/app.js +7 -0
  32. package/tools/dashboard/dashboard_snapshot.py +13 -29
  33. package/SKILL.md +0 -149
@@ -0,0 +1,506 @@
1
+ #!/usr/bin/env bash
2
+ # heartbeat-loop-scheduling-lib.sh — Issue scheduling, ordering, and sync.
3
+ #
4
+ # Manages scheduled issues (cron-like), blocked-recovery queue, recurring
5
+ # issue rotation, issue/PR ID caching, and open-agent sync.
6
+ #
7
+ # Depends on: heartbeat-loop-worker-lib.sh, heartbeat-loop-cache-lib.sh
8
+
9
+ blocked_recovery_issue_ids() {
10
+ ensure_blocked_recovery_issue_ids_cache
11
+ printf '%s\n' "$blocked_recovery_issue_ids_cache"
12
+ }
13
+
14
+ ordered_ready_issue_ids() {
15
+ ensure_ordered_ready_issue_ids_cache
16
+ printf '%s\n' "$ordered_ready_issue_ids_cache"
17
+ }
18
+
19
+ due_scheduled_issue_ids() {
20
+ ensure_due_scheduled_issue_ids_cache
21
+ printf '%s\n' "$due_scheduled_issue_ids_cache"
22
+ }
23
+
24
+ due_blocked_recovery_issue_ids() {
25
+ ensure_due_blocked_recovery_issue_ids_cache
26
+ printf '%s\n' "$due_blocked_recovery_issue_ids_cache"
27
+ }
28
+
29
+ ensure_due_scheduled_issue_ids_cache() {
30
+ if [[ "$due_scheduled_issue_ids_cache_loaded" != "yes" ]]; then
31
+ due_scheduled_issue_ids_cache="$(build_due_scheduled_issue_ids_cache)"
32
+ due_scheduled_issue_ids_cache_loaded="yes"
33
+ fi
34
+ }
35
+
36
+ ensure_due_blocked_recovery_issue_ids_cache() {
37
+ if [[ "$due_blocked_recovery_issue_ids_cache_loaded" != "yes" ]]; then
38
+ due_blocked_recovery_issue_ids_cache="$(build_due_blocked_recovery_issue_ids_cache)"
39
+ due_blocked_recovery_issue_ids_cache_loaded="yes"
40
+ fi
41
+ }
42
+
43
+ build_due_scheduled_issue_ids_cache() {
44
+ local issue_id now_epoch due_epoch
45
+ now_epoch="$(date +%s)"
46
+ ensure_ready_issue_ids_cache
47
+ while IFS= read -r issue_id; do
48
+ [[ -n "$issue_id" ]] || continue
49
+ if [[ "$(cached_issue_attr scheduled "$issue_id")" != "yes" ]]; then
50
+ continue
51
+ fi
52
+ if ! scheduled_issue_is_due "$issue_id"; then
53
+ continue
54
+ fi
55
+ due_epoch="$(scheduled_issue_due_epoch "$issue_id")"
56
+ if ! [[ "${due_epoch:-}" =~ ^[0-9]+$ ]]; then
57
+ due_epoch=0
58
+ fi
59
+ printf '%s\t%s\n' "$due_epoch" "$issue_id"
60
+ done <<<"$ready_issue_ids_cache" | sort -n -k1,1 -k2,2n | cut -f2
61
+ }
62
+
63
+ build_due_blocked_recovery_issue_ids_cache() {
64
+ local issue_id due_epoch
65
+ if (( max_concurrent_blocked_recovery_issue_workers <= 0 )); then
66
+ return 0
67
+ fi
68
+
69
+ ensure_blocked_recovery_issue_ids_cache
70
+ while IFS= read -r issue_id; do
71
+ [[ -n "$issue_id" ]] || continue
72
+ if ! blocked_recovery_issue_is_due "$issue_id"; then
73
+ continue
74
+ fi
75
+ due_epoch="$(blocked_recovery_issue_due_epoch "$issue_id")"
76
+ if ! [[ "${due_epoch:-}" =~ ^[0-9]+$ ]]; then
77
+ due_epoch=0
78
+ fi
79
+ printf '%s\t%s\n' "$due_epoch" "$issue_id"
80
+ done <<<"$blocked_recovery_issue_ids_cache" | sort -n -k1,1 -k2,2n | cut -f2
81
+ }
82
+
83
+ build_ordered_ready_issue_ids_cache() {
84
+ local issue_id is_recurring last_recurring_issue seen_last="no"
85
+ local -a recurring_ids=()
86
+ ensure_ready_issue_ids_cache
87
+ while IFS= read -r issue_id; do
88
+ [[ -n "$issue_id" ]] || continue
89
+ if [[ "$(cached_issue_attr scheduled "$issue_id")" == "yes" ]]; then
90
+ continue
91
+ fi
92
+ is_recurring="$(cached_issue_attr recurring "$issue_id")"
93
+ if [[ "$is_recurring" != "yes" ]]; then
94
+ printf '%s\n' "$issue_id"
95
+ else
96
+ recurring_ids+=("$issue_id")
97
+ fi
98
+ done <<<"$ready_issue_ids_cache"
99
+
100
+ if (( ${#recurring_ids[@]} == 0 )); then
101
+ return 0
102
+ fi
103
+
104
+ last_recurring_issue="$(last_launched_recurring_issue_id || true)"
105
+ if [[ -n "$last_recurring_issue" ]]; then
106
+ local emitted_after_last=0
107
+ for issue_id in "${recurring_ids[@]}"; do
108
+ if [[ "$seen_last" == "yes" ]]; then
109
+ printf '%s\n' "$issue_id"
110
+ emitted_after_last=$((emitted_after_last + 1))
111
+ fi
112
+ if [[ "$issue_id" == "$last_recurring_issue" ]]; then
113
+ seen_last="yes"
114
+ fi
115
+ done
116
+ fi
117
+
118
+ for issue_id in "${recurring_ids[@]}"; do
119
+ # Stop the wrap-around once we reach the last-launched issue, but only
120
+ # when the first loop already emitted at least one issue after it.
121
+ # When there is exactly one recurring issue (or the last-launched issue
122
+ # is the final element), emitted_after_last is 0, so we must still
123
+ # include it here to avoid producing an empty list.
124
+ if [[ -n "$last_recurring_issue" && "$seen_last" == "yes" && "$issue_id" == "$last_recurring_issue" && "$emitted_after_last" -gt 0 ]]; then
125
+ break
126
+ fi
127
+ printf '%s\n' "$issue_id"
128
+ done
129
+ }
130
+
131
+ completed_workers() {
132
+ ensure_completed_workers_cache
133
+ printf '%s\n' "$completed_workers_cache"
134
+ }
135
+
136
+ reconciled_marker_matches_run() {
137
+ local run_dir="${1:?run dir required}"
138
+ local marker_file="${run_dir}/reconciled.ok"
139
+ local run_env="${run_dir}/run.env"
140
+ local marker_started_at=""
141
+ local run_started_at=""
142
+
143
+ [[ -f "${marker_file}" && -f "${run_env}" ]] || return 1
144
+
145
+ marker_started_at="$(awk -F= '/^STARTED_AT=/{print $2}' "${marker_file}" 2>/dev/null | tr -d '"' | tail -n 1 || true)"
146
+ run_started_at="$(awk -F= '/^STARTED_AT=/{print $2}' "${run_env}" 2>/dev/null | tr -d '"' | tail -n 1 || true)"
147
+
148
+ [[ -n "${marker_started_at}" && -n "${run_started_at}" && "${marker_started_at}" == "${run_started_at}" ]]
149
+ }
150
+
151
+ ensure_completed_workers_cache() {
152
+ local dir session issue_id status_line status
153
+ if [[ "$completed_workers_cache_loaded" == "yes" ]]; then
154
+ return 0
155
+ fi
156
+ completed_workers_cache=""
157
+ for dir in "$runs_root"/*; do
158
+ [[ -d "$dir" ]] || continue
159
+ session="${dir##*/}"
160
+ session_matches_prefix "$session" || continue
161
+ if reconciled_marker_matches_run "$dir"; then
162
+ continue
163
+ fi
164
+ if [[ "$session" == "${issue_prefix}"* ]]; then
165
+ issue_id="$(issue_id_from_session "$session" || true)"
166
+ if [[ -n "${issue_id}" ]] && pending_issue_launch_active "${issue_id}"; then
167
+ continue
168
+ fi
169
+ fi
170
+ status_line="$(
171
+ "${shared_agent_home}/tools/bin/agent-project-worker-status" \
172
+ --runs-root "$runs_root" \
173
+ --session "$session" \
174
+ | awk -F= '/^STATUS=/{print $2}' || true
175
+ )"
176
+ status="${status_line:-UNKNOWN}"
177
+ if [[ "$status" == "SUCCEEDED" || "$status" == "FAILED" ]]; then
178
+ completed_workers_cache+="${session}"$'\n'
179
+ fi
180
+ done
181
+ completed_workers_cache="${completed_workers_cache%$'\n'}"
182
+ completed_workers_cache_loaded="yes"
183
+ }
184
+
185
+ ready_issue_ids() {
186
+ ensure_ready_issue_ids_cache
187
+ printf '%s\n' "$ready_issue_ids_cache"
188
+ }
189
+
190
+ ensure_ready_issue_ids_cache() {
191
+ if [[ "$ready_issue_ids_cache_loaded" != "yes" ]]; then
192
+ ready_issue_ids_cache="$(heartbeat_list_ready_issue_ids)"
193
+ ready_issue_ids_cache_loaded="yes"
194
+ fi
195
+ }
196
+
197
+ last_launched_recurring_issue_id() {
198
+ if [[ -f "$recurring_rotation_file" ]]; then
199
+ tr -d '[:space:]' <"$recurring_rotation_file"
200
+ fi
201
+ }
202
+
203
+ record_recurring_issue_launch() {
204
+ local issue_id="${1:?issue id required}"
205
+ printf '%s\n' "$issue_id" >"$recurring_rotation_file"
206
+ }
207
+
208
+ scheduled_state_file() {
209
+ local issue_id="${1:?issue id required}"
210
+ printf '%s\n' "${scheduled_state_dir}/${issue_id}.env"
211
+ }
212
+
213
+ scheduled_issue_due_epoch() {
214
+ local issue_id="${1:?issue id required}"
215
+ local state_file next_due_epoch
216
+ state_file="$(scheduled_state_file "$issue_id")"
217
+ if [[ ! -f "$state_file" ]]; then
218
+ printf '0\n'
219
+ return 0
220
+ fi
221
+
222
+ next_due_epoch="$(awk -F= '/^NEXT_DUE_EPOCH=/{print $2}' "$state_file" 2>/dev/null | tr -d '[:space:]' || true)"
223
+ if ! [[ "${next_due_epoch:-}" =~ ^[0-9]+$ ]]; then
224
+ printf '0\n'
225
+ return 0
226
+ fi
227
+
228
+ printf '%s\n' "$next_due_epoch"
229
+ }
230
+
231
+ scheduled_issue_is_due() {
232
+ local issue_id="${1:?issue id required}"
233
+ local interval_seconds due_epoch now_epoch
234
+ interval_seconds="$(cached_issue_attr schedule_interval_seconds "$issue_id")"
235
+ if ! [[ "${interval_seconds:-}" =~ ^[1-9][0-9]*$ ]]; then
236
+ return 1
237
+ fi
238
+
239
+ due_epoch="$(scheduled_issue_due_epoch "$issue_id")"
240
+ now_epoch="$(date +%s)"
241
+ if ! [[ "${due_epoch:-}" =~ ^[0-9]+$ ]] || (( due_epoch == 0 || due_epoch <= now_epoch )); then
242
+ return 0
243
+ fi
244
+ return 1
245
+ }
246
+
247
+ record_scheduled_issue_launch() {
248
+ local issue_id="${1:?issue id required}"
249
+ local interval_seconds state_file now_epoch due_epoch next_due_epoch
250
+
251
+ interval_seconds="$(cached_issue_attr schedule_interval_seconds "$issue_id")"
252
+ if ! [[ "${interval_seconds:-}" =~ ^[1-9][0-9]*$ ]]; then
253
+ return 0
254
+ fi
255
+
256
+ now_epoch="$(date +%s)"
257
+ due_epoch="$(scheduled_issue_due_epoch "$issue_id")"
258
+ if ! [[ "${due_epoch:-}" =~ ^[0-9]+$ ]] || (( due_epoch <= 0 )); then
259
+ next_due_epoch=$((now_epoch + interval_seconds))
260
+ else
261
+ next_due_epoch="$due_epoch"
262
+ while (( next_due_epoch <= now_epoch )); do
263
+ next_due_epoch=$((next_due_epoch + interval_seconds))
264
+ done
265
+ fi
266
+
267
+ state_file="$(scheduled_state_file "$issue_id")"
268
+ cat >"$state_file" <<EOF
269
+ INTERVAL_SECONDS=${interval_seconds}
270
+ LAST_STARTED_EPOCH=${now_epoch}
271
+ LAST_STARTED_AT=$(date -u -r "$now_epoch" +"%Y-%m-%dT%H:%M:%SZ")
272
+ NEXT_DUE_EPOCH=${next_due_epoch}
273
+ NEXT_DUE_AT=$(date -u -r "$next_due_epoch" +"%Y-%m-%dT%H:%M:%SZ")
274
+ UPDATED_AT=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
275
+ EOF
276
+ }
277
+
278
+ record_scheduled_issue_result() {
279
+ local issue_id="${1:?issue id required}"
280
+ local result_status="${2:-unknown}"
281
+ local state_file interval_seconds last_started_epoch next_due_epoch now_epoch
282
+
283
+ state_file="$(scheduled_state_file "$issue_id")"
284
+ interval_seconds="$(cached_issue_attr schedule_interval_seconds "$issue_id")"
285
+ last_started_epoch="$(awk -F= '/^LAST_STARTED_EPOCH=/{print $2}' "$state_file" 2>/dev/null | tr -d '[:space:]' || true)"
286
+ next_due_epoch="$(awk -F= '/^NEXT_DUE_EPOCH=/{print $2}' "$state_file" 2>/dev/null | tr -d '[:space:]' || true)"
287
+ now_epoch="$(date +%s)"
288
+
289
+ if ! [[ "${interval_seconds:-}" =~ ^[1-9][0-9]*$ ]]; then
290
+ interval_seconds=0
291
+ fi
292
+ if ! [[ "${last_started_epoch:-}" =~ ^[0-9]+$ ]]; then
293
+ last_started_epoch=0
294
+ fi
295
+ if ! [[ "${next_due_epoch:-}" =~ ^[0-9]+$ ]]; then
296
+ next_due_epoch=0
297
+ fi
298
+
299
+ cat >"$state_file" <<EOF
300
+ INTERVAL_SECONDS=${interval_seconds}
301
+ LAST_STARTED_EPOCH=${last_started_epoch}
302
+ LAST_STARTED_AT=$(if [[ "$last_started_epoch" =~ ^[0-9]+$ ]] && (( last_started_epoch > 0 )); then date -u -r "$last_started_epoch" +"%Y-%m-%dT%H:%M:%SZ"; fi)
303
+ LAST_RESULT_STATUS=${result_status}
304
+ LAST_RESULT_EPOCH=${now_epoch}
305
+ LAST_RESULT_AT=$(date -u -r "$now_epoch" +"%Y-%m-%dT%H:%M:%SZ")
306
+ NEXT_DUE_EPOCH=${next_due_epoch}
307
+ NEXT_DUE_AT=$(if [[ "$next_due_epoch" =~ ^[0-9]+$ ]] && (( next_due_epoch > 0 )); then date -u -r "$next_due_epoch" +"%Y-%m-%dT%H:%M:%SZ"; fi)
308
+ UPDATED_AT=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
309
+ EOF
310
+ }
311
+
312
+ blocked_recovery_state_file() {
313
+ local issue_id="${1:?issue id required}"
314
+ printf '%s\n' "${blocked_recovery_state_dir}/${issue_id}.env"
315
+ }
316
+
317
+ blocked_recovery_issue_has_state() {
318
+ local issue_id="${1:?issue id required}"
319
+ [[ -f "$(blocked_recovery_state_file "$issue_id")" ]]
320
+ }
321
+
322
+ blocked_recovery_issue_due_epoch() {
323
+ local issue_id="${1:?issue id required}"
324
+ local state_file next_due_epoch
325
+ state_file="$(blocked_recovery_state_file "$issue_id")"
326
+ if [[ ! -f "$state_file" ]]; then
327
+ printf '0\n'
328
+ return 0
329
+ fi
330
+
331
+ next_due_epoch="$(awk -F= '/^NEXT_DUE_EPOCH=/{print $2}' "$state_file" 2>/dev/null | tr -d '[:space:]' || true)"
332
+ if ! [[ "${next_due_epoch:-}" =~ ^[0-9]+$ ]]; then
333
+ printf '0\n'
334
+ return 0
335
+ fi
336
+
337
+ printf '%s\n' "$next_due_epoch"
338
+ }
339
+
340
+ blocked_recovery_issue_is_due() {
341
+ local issue_id="${1:?issue id required}"
342
+ local due_epoch now_epoch
343
+ if ! [[ "${blocked_recovery_cooldown_seconds:-}" =~ ^[1-9][0-9]*$ ]]; then
344
+ return 0
345
+ fi
346
+
347
+ due_epoch="$(blocked_recovery_issue_due_epoch "$issue_id")"
348
+ now_epoch="$(date +%s)"
349
+ if ! [[ "${due_epoch:-}" =~ ^[0-9]+$ ]] || (( due_epoch == 0 || due_epoch <= now_epoch )); then
350
+ return 0
351
+ fi
352
+ return 1
353
+ }
354
+
355
+ record_blocked_recovery_issue_launch() {
356
+ local issue_id="${1:?issue id required}"
357
+ local state_file now_epoch next_due_epoch next_due_at
358
+
359
+ now_epoch="$(date +%s)"
360
+ next_due_epoch=0
361
+ next_due_at=""
362
+ if [[ "${blocked_recovery_cooldown_seconds:-}" =~ ^[1-9][0-9]*$ ]]; then
363
+ next_due_epoch=$((now_epoch + blocked_recovery_cooldown_seconds))
364
+ next_due_at="$(date -u -r "$next_due_epoch" +"%Y-%m-%dT%H:%M:%SZ")"
365
+ fi
366
+
367
+ state_file="$(blocked_recovery_state_file "$issue_id")"
368
+ cat >"$state_file" <<EOF
369
+ LANE=blocked-recovery
370
+ LAST_STARTED_EPOCH=${now_epoch}
371
+ LAST_STARTED_AT=$(date -u -r "$now_epoch" +"%Y-%m-%dT%H:%M:%SZ")
372
+ NEXT_DUE_EPOCH=${next_due_epoch}
373
+ NEXT_DUE_AT=${next_due_at}
374
+ UPDATED_AT=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
375
+ EOF
376
+ }
377
+
378
+ clear_blocked_recovery_issue_state() {
379
+ local issue_id="${1:?issue id required}"
380
+ rm -f "$(blocked_recovery_state_file "$issue_id")"
381
+ }
382
+
383
+ open_agent_pr_ids() {
384
+ ensure_open_agent_pr_ids_cache
385
+ printf '%s\n' "$open_agent_pr_ids_cache"
386
+ }
387
+
388
+ ensure_open_agent_pr_ids_cache() {
389
+ if [[ "$open_agent_pr_ids_cache_loaded" != "yes" ]]; then
390
+ open_agent_pr_ids_cache="$(heartbeat_list_open_agent_pr_ids)"
391
+ open_agent_pr_ids_cache_loaded="yes"
392
+ fi
393
+ }
394
+
395
+ running_issue_ids() {
396
+ ensure_running_issue_ids_cache
397
+ printf '%s\n' "$running_issue_ids_cache"
398
+ }
399
+
400
+ exclusive_issue_ids() {
401
+ ensure_exclusive_issue_ids_cache
402
+ printf '%s\n' "$exclusive_issue_ids_cache"
403
+ }
404
+
405
+ exclusive_pr_ids() {
406
+ ensure_exclusive_pr_ids_cache
407
+ printf '%s\n' "$exclusive_pr_ids_cache"
408
+ }
409
+
410
+ ensure_running_issue_ids_cache() {
411
+ if [[ "$running_issue_ids_cache_loaded" != "yes" ]]; then
412
+ running_issue_ids_cache="$(heartbeat_list_running_issue_ids)"
413
+ running_issue_ids_cache_loaded="yes"
414
+ fi
415
+ }
416
+
417
+ ensure_exclusive_issue_ids_cache() {
418
+ if [[ "$exclusive_issue_ids_cache_loaded" != "yes" ]]; then
419
+ exclusive_issue_ids_cache="$(heartbeat_list_exclusive_issue_ids)"
420
+ exclusive_issue_ids_cache_loaded="yes"
421
+ fi
422
+ }
423
+
424
+ ensure_exclusive_pr_ids_cache() {
425
+ if [[ "$exclusive_pr_ids_cache_loaded" != "yes" ]]; then
426
+ exclusive_pr_ids_cache="$(heartbeat_list_exclusive_pr_ids)"
427
+ exclusive_pr_ids_cache_loaded="yes"
428
+ fi
429
+ }
430
+
431
+ ensure_ordered_ready_issue_ids_cache() {
432
+ if [[ "$ordered_ready_issue_ids_cache_loaded" != "yes" ]]; then
433
+ ordered_ready_issue_ids_cache="$(build_ordered_ready_issue_ids_cache)"
434
+ ordered_ready_issue_ids_cache_loaded="yes"
435
+ fi
436
+ }
437
+
438
+ ensure_blocked_recovery_issue_ids_cache() {
439
+ if [[ "$blocked_recovery_issue_ids_cache_loaded" != "yes" ]]; then
440
+ blocked_recovery_issue_ids_cache="$(heartbeat_list_blocked_recovery_issue_ids)"
441
+ blocked_recovery_issue_ids_cache_loaded="yes"
442
+ fi
443
+ }
444
+
445
+ sync_open_agent_issues() {
446
+ local issue_id status_out status
447
+ ensure_running_issue_ids_cache
448
+ while IFS= read -r issue_id; do
449
+ [[ -n "$issue_id" ]] || continue
450
+ if tmux has-session -t "${issue_prefix}${issue_id}" 2>/dev/null; then
451
+ continue
452
+ fi
453
+ if pending_issue_launch_active "$issue_id"; then
454
+ if pending_issue_launch_counts_toward_capacity "$issue_id"; then
455
+ heartbeat_mark_issue_running "$issue_id" "$(cached_issue_attr heavy "$issue_id")" >/dev/null || true
456
+ fi
457
+ continue
458
+ fi
459
+ status_out="$(
460
+ "${shared_agent_home}/tools/bin/agent-project-worker-status" \
461
+ --runs-root "$runs_root" \
462
+ --session "${issue_prefix}${issue_id}"
463
+ )"
464
+ status="$(awk -F= '/^STATUS=/{print $2}' <<<"$status_out")"
465
+ case "$status" in
466
+ RUNNING)
467
+ ;;
468
+ *)
469
+ heartbeat_sync_issue_labels "$issue_id" >/dev/null || true
470
+ ;;
471
+ esac
472
+ done <<<"$running_issue_ids_cache"
473
+ }
474
+
475
+ sync_open_agent_prs() {
476
+ local pr_number status_out status
477
+ ensure_open_agent_pr_ids_cache
478
+ while IFS= read -r pr_number; do
479
+ [[ -n "$pr_number" ]] || continue
480
+ if tmux has-session -t "${pr_prefix}${pr_number}" 2>/dev/null; then
481
+ continue
482
+ fi
483
+ if pending_pr_launch_active "$pr_number"; then
484
+ heartbeat_mark_pr_running "$pr_number" >/dev/null || true
485
+ continue
486
+ fi
487
+ status_out="$(
488
+ "${shared_agent_home}/tools/bin/agent-project-worker-status" \
489
+ --runs-root "$runs_root" \
490
+ --session "${pr_prefix}${pr_number}"
491
+ )"
492
+ status="$(awk -F= '/^STATUS=/{print $2}' <<<"$status_out")"
493
+ case "$status" in
494
+ UNKNOWN)
495
+ heartbeat_clear_pr_running "$pr_number" >/dev/null || true
496
+ heartbeat_sync_pr_labels "$pr_number" >/dev/null || true
497
+ ;;
498
+ RUNNING)
499
+ ;;
500
+ *)
501
+ heartbeat_clear_pr_running "$pr_number" >/dev/null || true
502
+ heartbeat_sync_pr_labels "$pr_number" >/dev/null || true
503
+ ;;
504
+ esac
505
+ done <<<"$open_agent_pr_ids_cache"
506
+ }