agent-control-plane 0.1.13 → 0.1.16

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.
@@ -361,7 +361,7 @@ print_status() {
361
361
  if [[ -n "${heartbeat}" || -n "${shared_loop}" || -n "${supervisor}" || "${controller_count}" != "0" || "${active_session_count}" != "0" ]]; then
362
362
  runtime_status="running"
363
363
  fi
364
- if [[ -z "${heartbeat}" && -z "${supervisor}" && ( -n "${shared_loop}" || "${controller_count}" != "0" || "${active_session_count}" != "0" ) ]]; then
364
+ if [[ -z "${heartbeat}" && -z "${supervisor}" && "${active_session_count}" == "0" && ( -n "${shared_loop}" || "${controller_count}" != "0" ) ]]; then
365
365
  runtime_status="partial"
366
366
  fi
367
367
 
@@ -20,6 +20,7 @@ backend=""
20
20
  model=""
21
21
  action=""
22
22
  reason=""
23
+ label=""
23
24
 
24
25
  case "$#" in
25
26
  1)
@@ -77,6 +78,33 @@ resolve_backend() {
77
78
  flow_config_get "${CONFIG_YAML}" "execution.coding_worker"
78
79
  }
79
80
 
81
+ resolve_codex_label() {
82
+ local configured_label="${ACP_ACTIVE_PROVIDER_LABEL:-${F_LOSNING_ACTIVE_PROVIDER_LABEL:-}}"
83
+ local codex_quota_bin=""
84
+ local active_label=""
85
+
86
+ if [[ -n "${configured_label}" ]]; then
87
+ printf '%s\n' "${configured_label}"
88
+ return 0
89
+ fi
90
+
91
+ if [[ -n "${ACP_CODEX_QUOTA_LABEL:-${F_LOSNING_CODEX_QUOTA_LABEL:-}}" ]]; then
92
+ printf '%s\n' "${ACP_CODEX_QUOTA_LABEL:-${F_LOSNING_CODEX_QUOTA_LABEL:-}}"
93
+ return 0
94
+ fi
95
+
96
+ codex_quota_bin="$(flow_resolve_codex_quota_bin "${SCRIPT_DIR}")"
97
+ if [[ -n "${codex_quota_bin}" && -x "${codex_quota_bin}" ]]; then
98
+ active_label="$("${codex_quota_bin}" codex list --json 2>/dev/null | jq -r '.activeInfo.trackedLabel // .activeInfo.activeLabel // empty' 2>/dev/null || true)"
99
+ if [[ -n "${active_label}" ]]; then
100
+ printf '%s\n' "${active_label}"
101
+ return 0
102
+ fi
103
+ fi
104
+
105
+ return 1
106
+ }
107
+
80
108
  resolve_model() {
81
109
  local resolved_backend="${1:?backend required}"
82
110
  local raw_model="${2:-}"
@@ -147,7 +175,16 @@ case "${action}" in
147
175
  ;;
148
176
  esac
149
177
 
150
- provider_key="$(flow_sanitize_provider_key "${backend}-${model}")"
178
+ if [[ "${backend}" == "codex" ]]; then
179
+ label="$(resolve_codex_label || true)"
180
+ fi
181
+
182
+ provider_key_source="${backend}-${model}"
183
+ if [[ "${backend}" == "codex" && -n "${label}" ]]; then
184
+ provider_key_source="${provider_key_source}-${label}"
185
+ fi
186
+
187
+ provider_key="$(flow_sanitize_provider_key "${provider_key_source}")"
151
188
  out="$(
152
189
  ACP_STATE_ROOT="${STATE_ROOT}" \
153
190
  ACP_PROVIDER_QUOTA_COOLDOWNS="${COOLDOWNS}" \
@@ -162,5 +199,6 @@ out="$(
162
199
 
163
200
  printf 'BACKEND=%s\n' "${backend}"
164
201
  printf 'MODEL=%s\n' "${model}"
202
+ printf 'LABEL=%s\n' "${label}"
165
203
  printf 'PROVIDER_KEY=%s\n' "${provider_key}"
166
204
  printf '%s\n' "${out}"
@@ -54,6 +54,9 @@ rollback_labels_on_failure() {
54
54
  if [[ "${label_rollback_armed}" != "yes" || "${launch_success}" == "yes" ]]; then
55
55
  return 0
56
56
  fi
57
+ if [[ -d "${RUN_DIR}" && ! -f "${RUN_DIR}/run.env" && ! -f "${RUN_DIR}/runner.env" && ! -f "${RUN_DIR}/result.env" ]]; then
58
+ rm -rf "${RUN_DIR}" >/dev/null 2>&1 || true
59
+ fi
57
60
  if [[ -x "${UPDATE_LABELS_BIN}" ]]; then
58
61
  bash "${UPDATE_LABELS_BIN}" --repo-slug "${REPO_SLUG}" --number "${ISSUE_ID}" --remove agent-running >/dev/null 2>&1 || true
59
62
  fi
@@ -377,6 +380,30 @@ const recentPrs = recentNumbers.map((number) => {
377
380
  const activePrs = recentPrs.filter((pr) => pr.state === 'open' || pr.state === 'draft');
378
381
  const completedPrs = recentPrs.filter((pr) => pr.state !== 'open' && pr.state !== 'draft');
379
382
 
383
+ const recentCycleNotes = [];
384
+ for (const comment of [...(issue.comments || [])].reverse()) {
385
+ const body = String(comment?.body || '').trim();
386
+ if (!body) {
387
+ continue;
388
+ }
389
+ if (!/^(Completed|Blocked on|# Blocker:|Host-side publish blocked|Host-side publish failed)/im.test(body)) {
390
+ continue;
391
+ }
392
+ const summaryLines = body
393
+ .split(/\r?\n/)
394
+ .map((line) => line.trim())
395
+ .filter(Boolean)
396
+ .slice(0, 6);
397
+ if (summaryLines.length === 0) {
398
+ continue;
399
+ }
400
+ const summary = summaryLines.join(' | ');
401
+ recentCycleNotes.push(summary.length > 420 ? `${summary.slice(0, 417)}...` : summary);
402
+ if (recentCycleNotes.length >= 3) {
403
+ break;
404
+ }
405
+ }
406
+
380
407
  const formatPr = (pr) => {
381
408
  const suffix = pr.url ? ` ${pr.url}` : '';
382
409
  return `- #${pr.number} (${pr.state}): ${pr.title}${suffix}`;
@@ -389,6 +416,7 @@ const lines = [
389
416
  '- Before editing, choose exactly one concrete target module, screen, or flow and keep the cycle limited to that target.',
390
417
  '- Do not work on a target already covered by an open or draft PR for this issue, or by the most recent completed cycles listed below, unless you are explicitly fixing a regression introduced there.',
391
418
  '- If you cannot identify a small non-overlapping target after reviewing recent cycle history, stop blocked using the blocker contract instead of forcing another PR.',
419
+ '- Prefer the recent cycle notes below over repeating broad web research; only fetch outside context when the local baseline or linked advisories materially changed.',
392
420
  '- In your final worker output, start with `Target:` and `Why now:` lines before the changed-files list.',
393
421
  ];
394
422
 
@@ -406,6 +434,13 @@ if (completedPrs.length > 0) {
406
434
  }
407
435
  }
408
436
 
437
+ if (recentCycleNotes.length > 0) {
438
+ lines.push('', '### Recent cycle notes from issue comments');
439
+ for (const note of recentCycleNotes) {
440
+ lines.push(`- ${note}`);
441
+ }
442
+ }
443
+
409
444
  process.stdout.write(`${lines.join('\n')}\n`);
410
445
  EOF
411
446
  ISSUE_RECURRING_CONTEXT="$(cat "$ISSUE_RECURRING_CONTEXT_FILE")"
@@ -33,6 +33,9 @@ rollback_labels_on_failure() {
33
33
  if [[ "${label_rollback_armed}" != "yes" || "${launch_success}" == "yes" ]]; then
34
34
  return 0
35
35
  fi
36
+ if [[ -d "${RUN_DIR}" && ! -f "${RUN_DIR}/run.env" && ! -f "${RUN_DIR}/runner.env" && ! -f "${RUN_DIR}/result.env" ]]; then
37
+ rm -rf "${RUN_DIR}" >/dev/null 2>&1 || true
38
+ fi
36
39
  if [[ -x "${UPDATE_LABELS_BIN}" ]]; then
37
40
  bash "${UPDATE_LABELS_BIN}" --repo-slug "${REPO_SLUG}" --number "${PR_NUMBER}" --remove agent-running >/dev/null 2>&1 || true
38
41
  fi
@@ -32,6 +32,9 @@ rollback_labels_on_failure() {
32
32
  if [[ "${label_rollback_armed}" != "yes" || "${launch_success}" == "yes" ]]; then
33
33
  return 0
34
34
  fi
35
+ if [[ -d "${RUN_DIR}" && ! -f "${RUN_DIR}/run.env" && ! -f "${RUN_DIR}/runner.env" && ! -f "${RUN_DIR}/result.env" ]]; then
36
+ rm -rf "${RUN_DIR}" >/dev/null 2>&1 || true
37
+ fi
35
38
  if [[ -x "${UPDATE_LABELS_BIN}" ]]; then
36
39
  bash "${UPDATE_LABELS_BIN}" --repo-slug "${REPO_SLUG}" --number "${PR_NUMBER}" --remove agent-running >/dev/null 2>&1 || true
37
40
  fi
@@ -204,6 +204,7 @@ controller_refresh_execution_context() {
204
204
  ACP_OPENCLAW_MODEL F_LOSNING_OPENCLAW_MODEL \
205
205
  ACP_OPENCLAW_THINKING F_LOSNING_OPENCLAW_THINKING \
206
206
  ACP_OPENCLAW_TIMEOUT_SECONDS F_LOSNING_OPENCLAW_TIMEOUT_SECONDS \
207
+ ACP_OPENCLAW_STALL_SECONDS F_LOSNING_OPENCLAW_STALL_SECONDS \
207
208
  ACP_ACTIVE_PROVIDER_POOL_NAME F_LOSNING_ACTIVE_PROVIDER_POOL_NAME \
208
209
  ACP_ACTIVE_PROVIDER_BACKEND F_LOSNING_ACTIVE_PROVIDER_BACKEND \
209
210
  ACP_ACTIVE_PROVIDER_MODEL F_LOSNING_ACTIVE_PROVIDER_MODEL \
@@ -1,9 +1,56 @@
1
1
  const refreshButton = document.querySelector("#refresh-button");
2
+ const themeToggleButton = document.querySelector("#theme-toggle");
2
3
  const generatedAtNode = document.querySelector("#generated-at");
3
4
  const overviewNode = document.querySelector("#overview");
4
5
  const profilesNode = document.querySelector("#profiles");
5
6
  const seenAlertIds = new Set();
6
7
  let notificationPermissionRequested = false;
8
+ const THEME_STORAGE_KEY = "acp-dashboard-theme";
9
+
10
+ function systemPrefersDark() {
11
+ return typeof window.matchMedia === "function" && window.matchMedia("(prefers-color-scheme: dark)").matches;
12
+ }
13
+
14
+ function currentThemePreference() {
15
+ try {
16
+ const stored = window.localStorage.getItem(THEME_STORAGE_KEY);
17
+ if (stored === "light" || stored === "dark") return stored;
18
+ } catch (_error) {
19
+ // Ignore storage access issues and fall back to system preference.
20
+ }
21
+ return systemPrefersDark() ? "dark" : "light";
22
+ }
23
+
24
+ function updateThemeToggleLabel(theme) {
25
+ if (!themeToggleButton) return;
26
+ const nextTheme = theme === "dark" ? "light" : "dark";
27
+ const label = nextTheme === "dark" ? "Dark mode" : "Light mode";
28
+ themeToggleButton.textContent = label;
29
+ themeToggleButton.setAttribute("aria-label", `Switch to ${label.toLowerCase()}`);
30
+ }
31
+
32
+ function applyTheme(theme) {
33
+ document.documentElement.dataset.theme = theme;
34
+ updateThemeToggleLabel(theme);
35
+ }
36
+
37
+ function persistTheme(theme) {
38
+ try {
39
+ window.localStorage.setItem(THEME_STORAGE_KEY, theme);
40
+ } catch (_error) {
41
+ // Ignore storage access issues.
42
+ }
43
+ }
44
+
45
+ function initializeTheme() {
46
+ applyTheme(currentThemePreference());
47
+ if (!themeToggleButton) return;
48
+ themeToggleButton.addEventListener("click", () => {
49
+ const nextTheme = document.documentElement.dataset.theme === "dark" ? "light" : "dark";
50
+ applyTheme(nextTheme);
51
+ persistTheme(nextTheme);
52
+ });
53
+ }
7
54
 
8
55
  function relativeTime(input) {
9
56
  if (!input) return "n/a";
@@ -138,6 +185,33 @@ function renderAlerts(alerts) {
138
185
  `;
139
186
  }
140
187
 
188
+ function renderCodexRotation(rotation) {
189
+ if (!rotation || !rotation.active_label) {
190
+ return `<div class="empty-state">Codex rotation data is not available yet for this Codex profile.</div>`;
191
+ }
192
+ const candidates = (rotation.candidate_labels || []).length ? rotation.candidate_labels.join(", ") : "n/a";
193
+ const ready = (rotation.ready_candidates || []).length ? rotation.ready_candidates.join(", ") : "none";
194
+ const nextRetry = rotation.next_retry_at
195
+ ? `${rotation.next_retry_label || "n/a"} · ${relativeTime(rotation.next_retry_at)}<div class="muted">${rotation.next_retry_at}</div>`
196
+ : "n/a";
197
+ const lastSwitch = rotation.last_switch_label
198
+ ? `${rotation.last_switch_label}${rotation.last_switch_reason ? ` · ${rotation.last_switch_reason}` : ""}`
199
+ : "n/a";
200
+
201
+ return renderTable(
202
+ [
203
+ { label: "Current", render: () => `<div class="mono">${rotation.active_label}</div>` },
204
+ { label: "Decision", render: () => `<span class="status-pill ${statusClass(rotation.switch_decision || "unknown")}">${rotation.switch_decision || "unknown"}</span>` },
205
+ { label: "Candidates", render: () => `<div class="mono">${candidates}</div>` },
206
+ { label: "Ready now", render: () => `<div class="mono">${ready}</div>` },
207
+ { label: "Next retry", render: () => nextRetry },
208
+ { label: "Last switch", render: () => `<div class="mono">${lastSwitch}</div>` },
209
+ ],
210
+ [{}],
211
+ "No Codex rotation data for this profile.",
212
+ );
213
+ }
214
+
141
215
  function renderProfile(profile) {
142
216
  const providerBadges = [
143
217
  profile.coding_worker ? `<span class="badge good">${profile.coding_worker}</span>` : "",
@@ -153,6 +227,7 @@ function renderProfile(profile) {
153
227
  const summaryCards = [
154
228
  ["Run sessions", profile.counts.active_runs],
155
229
  ["Running", profile.counts.running_runs],
230
+ ["Recent completed", profile.counts.recent_history_runs || 0],
156
231
  ["Implemented", profile.counts.implemented_runs],
157
232
  ["Reported", profile.counts.reported_runs],
158
233
  ["Blocked", profile.counts.blocked_runs],
@@ -188,6 +263,19 @@ function renderProfile(profile) {
188
263
  "No active run directories for this profile.",
189
264
  );
190
265
 
266
+ const recentHistoryTable = renderTable(
267
+ [
268
+ { label: "Session", render: (row) => `<div class="mono">${row.session}</div>` },
269
+ { label: "Task", render: (row) => `${row.task_kind || "n/a"} ${row.task_id || ""}`.trim() },
270
+ { label: "Lifecycle", render: renderLifecycle },
271
+ { label: "Worker", key: "coding_worker" },
272
+ { label: "Result", render: renderResult },
273
+ { label: "Updated", render: (row) => row.updated_at ? `${relativeTime(row.updated_at)}<div class="muted">${row.updated_at}</div>` : "n/a" },
274
+ ],
275
+ profile.recent_history || [],
276
+ "No recently archived runs.",
277
+ );
278
+
191
279
  const controllerTable = renderTable(
192
280
  [
193
281
  { label: "Issue", key: "issue_id" },
@@ -214,6 +302,18 @@ function renderProfile(profile) {
214
302
  "No issue retries recorded.",
215
303
  );
216
304
 
305
+ const prRetryTable = renderTable(
306
+ [
307
+ { label: "PR", key: "pr_number" },
308
+ { label: "Status", render: (row) => `<span class="status-pill ${row.ready ? "" : "waiting-provider"}">${row.ready ? "ready" : "retrying"}</span>` },
309
+ { label: "Reason", render: (row) => row.last_reason || "n/a" },
310
+ { label: "Attempts", key: "attempts" },
311
+ { label: "Next attempt", render: (row) => row.next_attempt_at ? `${relativeTime(row.next_attempt_at)}<div class="muted">${row.next_attempt_at}</div>` : "n/a" },
312
+ ],
313
+ profile.pr_retries || [],
314
+ "No PR retries recorded.",
315
+ );
316
+
217
317
  const workerTable = renderTable(
218
318
  [
219
319
  { label: "Key", render: (row) => `<div class="mono">${row.key}</div>` },
@@ -261,6 +361,27 @@ function renderProfile(profile) {
261
361
  "No pending leased issues.",
262
362
  );
263
363
 
364
+ const claimsTable = renderTable(
365
+ [
366
+ { label: "Issue", key: "issue_id" },
367
+ { label: "Session", render: (row) => row.session ? `<div class="mono">${row.session}</div>` : "n/a" },
368
+ { label: "Updated", render: (row) => row.updated_at ? `${relativeTime(row.updated_at)}<div class="muted">${row.updated_at}</div>` : "n/a" },
369
+ ],
370
+ profile.issue_queue.claims || [],
371
+ "No claimed issues.",
372
+ );
373
+
374
+ const codexRotationPanel =
375
+ profile.coding_worker === "codex"
376
+ ? `
377
+ <section class="panel">
378
+ <h3>Codex Rotation</h3>
379
+ <p class="panel-subtitle">Shows the active Codex label, candidate labels, and whether failover is ready or deferred.</p>
380
+ ${renderCodexRotation(profile.codex_rotation)}
381
+ </section>
382
+ `
383
+ : "";
384
+
264
385
  return `
265
386
  <article class="profile">
266
387
  <header class="profile-header">
@@ -285,15 +406,25 @@ function renderProfile(profile) {
285
406
  <p class="panel-subtitle">Lifecycle shows technical session completion. Result shows what the run achieved: implemented, reported, or blocked.</p>
286
407
  ${runsTable}
287
408
  </section>
409
+ <section class="panel">
410
+ <h3>Recent Completed Runs</h3>
411
+ <p class="panel-subtitle">Recently archived runs so they do not disappear from the dashboard immediately after completion.</p>
412
+ ${recentHistoryTable}
413
+ </section>
288
414
  <section class="panel">
289
415
  <h3>Resident Controllers</h3>
290
416
  <p class="panel-subtitle">Includes provider wait and failover telemetry. Stale controllers show a warning.</p>
291
417
  ${controllerTable}
292
418
  </section>
419
+ ${codexRotationPanel}
293
420
  <section class="panel half">
294
421
  <h3>Issue Retries</h3>
295
422
  ${retryTable}
296
423
  </section>
424
+ <section class="panel half">
425
+ <h3>PR Retries</h3>
426
+ ${prRetryTable}
427
+ </section>
297
428
  <section class="panel">
298
429
  <h3>Resident Worker Metadata</h3>
299
430
  ${workerTable}
@@ -310,6 +441,10 @@ function renderProfile(profile) {
310
441
  <h3>Pending Issue Queue</h3>
311
442
  ${queueTable}
312
443
  </section>
444
+ <section class="panel half">
445
+ <h3>Claimed Issues</h3>
446
+ ${claimsTable}
447
+ </section>
313
448
  </section>
314
449
  </article>
315
450
  `;
@@ -368,6 +503,7 @@ refreshButton.addEventListener("click", () => {
368
503
  void loadSnapshot();
369
504
  });
370
505
 
506
+ initializeTheme();
371
507
  void loadSnapshot();
372
508
  window.setInterval(() => {
373
509
  void loadSnapshot();