hanoman 0.9.26 → 0.9.28

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 (26) hide show
  1. package/dist/build-info.json +3 -3
  2. package/dist/cli.js +86 -0
  3. package/dist/server.js +677 -207
  4. package/docs/agent-integration.md +1 -0
  5. package/package.json +1 -1
  6. package/web/assets/{DalangHanomanScreen-BMQjWbwC.js → DalangHanomanScreen-C3UMPHFg.js} +1 -1
  7. package/web/assets/{DocPreviewModal-Vysb114M.js → DocPreviewModal-Bp5stb4M.js} +1 -1
  8. package/web/assets/{DocsWorkspace-Bo2dJRqt.js → DocsWorkspace-C9yFxK4D.js} +1 -1
  9. package/web/assets/{IdeScreen-1V2L_82g.js → IdeScreen-CRFRAP6E.js} +1 -1
  10. package/web/assets/{LeadScreen-D7njTsNH.js → LeadScreen-DfVUawoK.js} +1 -1
  11. package/web/assets/{ReviewScreen-BytyE-o3.js → ReviewScreen-CMHOlmgX.js} +1 -1
  12. package/web/assets/{SchedulerScreen-b4hvb1Sy.js → SchedulerScreen-CK5QvX5c.js} +1 -1
  13. package/web/assets/{SettingsScreen-C9YeMz1R.js → SettingsScreen-D7P5JgjU.js} +1 -1
  14. package/web/assets/{TeamScreen-WvQNIYf0.js → TeamScreen-D4tA34Mj.js} +1 -1
  15. package/web/assets/TerminalScreen-BjFMJnq6.css +1 -0
  16. package/web/assets/TerminalScreen-CKySL63X.js +2 -0
  17. package/web/assets/{TriageScreen-CfzqvbI2.js → TriageScreen-B8ZCexaX.js} +1 -1
  18. package/web/assets/{VpsScreen-XWHjdq4i.js → VpsScreen-BCrLfZh7.js} +1 -1
  19. package/web/assets/{index-BvFCEJud.js → index-BRR3JL_o.js} +210 -210
  20. package/web/assets/{index-B8xChq6y.css → index-BqAP-VDq.css} +1 -1
  21. package/web/assets/{live-B2RFSX3l.js → live-DuBdI0MZ.js} +1 -1
  22. package/web/assets/{sched-lead-shared-0mpQqjza.js → sched-lead-shared-QqbtjoVv.js} +1 -1
  23. package/web/assets/{settings-rebase-BVFljdPX.js → settings-rebase-Cbh-JGJ-.js} +1 -1
  24. package/web/index.html +2 -2
  25. package/web/assets/TerminalScreen-BYx1OK27.js +0 -2
  26. package/web/assets/TerminalScreen-D0NdFmR-.css +0 -1
package/dist/server.js CHANGED
@@ -8381,6 +8381,8 @@ var init_api = __esm({
8381
8381
  // SPEC-199 · WebSocket siar dashboard (global, bukan per-sesi)
8382
8382
  // SPEC-919 · ADR-0147 · muat awal + fallback halaman Klien. Selama WS sehat tak ada yang men-poll-nya.
8383
8383
  presence: `${API}/presence`,
8384
+ // ADR-0220 · muat awal agen/MCP untuk agregat Overview; dashboard menerimanya lewat grup siar `overview`.
8385
+ overview: `${API}/overview`,
8384
8386
  wsTickets: `${API}/ws-tickets`,
8385
8387
  vps: `${API}/vps`,
8386
8388
  vpsOne: (id2) => `${API}/vps/${id2}`,
@@ -8721,12 +8723,88 @@ var init_session_kind = __esm({
8721
8723
  });
8722
8724
 
8723
8725
  // ../shared/src/session-end.ts
8726
+ function sessionOutcome(r) {
8727
+ if (!r.endedAt) return "running";
8728
+ if (r.endedReason === "reconciled") return "interrupted";
8729
+ return r.exitCode === null || r.exitCode === 0 ? "completed" : "failed";
8730
+ }
8724
8731
  var init_session_end = __esm({
8725
8732
  "../shared/src/session-end.ts"() {
8726
8733
  "use strict";
8727
8734
  }
8728
8735
  });
8729
8736
 
8737
+ // ../shared/src/overview.ts
8738
+ var zHeadlinePart, zOverviewDay, zOverviewRiver, zOverviewTokens, zOverviewWait, BENSIN_VERDICTS, zOverviewBensin, zOverviewHeatmap, zOverviewRecords, zOverview;
8739
+ var init_overview = __esm({
8740
+ "../shared/src/overview.ts"() {
8741
+ "use strict";
8742
+ init_zod();
8743
+ zHeadlinePart = external_exports.object({ text: external_exports.string(), tone: external_exports.enum(["bad", "good"]).optional() });
8744
+ zOverviewDay = external_exports.object({ landed: external_exports.number().int(), failed: external_exports.number().int() });
8745
+ zOverviewRiver = external_exports.object({
8746
+ created: external_exports.number().int(),
8747
+ started: external_exports.number().int(),
8748
+ landed: external_exports.number().int(),
8749
+ // Median 7 hari terakhir atas spec yang selesai; null bila tak ada.
8750
+ leadTimeMedMs: external_exports.number().nullable(),
8751
+ cycleTimeMedMs: external_exports.number().nullable()
8752
+ });
8753
+ zOverviewTokens = external_exports.object({
8754
+ input: external_exports.number(),
8755
+ output: external_exports.number(),
8756
+ cached: external_exports.number(),
8757
+ total: external_exports.number(),
8758
+ cacheHitPct: external_exports.number().nullable(),
8759
+ perLanded: external_exports.number().nullable(),
8760
+ topAgent: external_exports.object({ name: external_exports.string(), total: external_exports.number() }).nullable(),
8761
+ yesterdayTotal: external_exports.number()
8762
+ });
8763
+ zOverviewWait = external_exports.object({
8764
+ todayMin: external_exports.number(),
8765
+ yesterdayMin: external_exports.number(),
8766
+ pctOfLive: external_exports.number().nullable()
8767
+ });
8768
+ BENSIN_VERDICTS = ["habis", "aman", "stabil", "kurang-data"];
8769
+ zOverviewBensin = external_exports.object({
8770
+ source: external_exports.enum(["claude", "codex"]),
8771
+ key: external_exports.string(),
8772
+ label: external_exports.string(),
8773
+ usedPct: external_exports.number(),
8774
+ resetsAt: external_exports.string().nullable(),
8775
+ exhaustAt: external_exports.string().nullable(),
8776
+ verdict: external_exports.enum(BENSIN_VERDICTS),
8777
+ samples: external_exports.array(external_exports.object({ at: external_exports.string(), pct: external_exports.number() }))
8778
+ });
8779
+ zOverviewHeatmap = external_exports.object({
8780
+ days: external_exports.array(external_exports.string()),
8781
+ // YYYY-MM-DD lokal, terlama dulu
8782
+ minutes: external_exports.array(external_exports.array(external_exports.number()))
8783
+ // [hari][jam 0..23] = menit sesi agen hidup
8784
+ });
8785
+ zOverviewRecords = external_exports.object({
8786
+ streakDays: external_exports.number().int(),
8787
+ bestStreak: external_exports.number().int(),
8788
+ bestLandedDay: external_exports.object({ date: external_exports.string(), count: external_exports.number().int() }).nullable(),
8789
+ landedRecordToday: external_exports.boolean()
8790
+ });
8791
+ zOverview = external_exports.object({
8792
+ generatedAt: external_exports.string(),
8793
+ recordingSince: external_exports.string(),
8794
+ // awal perekam di memori (menunggu & bensin); restart mengulangnya
8795
+ today: zOverviewDay.nullable(),
8796
+ yesterday: zOverviewDay.nullable(),
8797
+ river: zOverviewRiver.nullable(),
8798
+ tokens: zOverviewTokens.nullable(),
8799
+ wait: zOverviewWait.nullable(),
8800
+ bensin: external_exports.array(zOverviewBensin).nullable(),
8801
+ heatmap: zOverviewHeatmap.nullable(),
8802
+ records: zOverviewRecords.nullable(),
8803
+ headline: external_exports.object({ text: external_exports.string(), parts: external_exports.array(zHeadlinePart) })
8804
+ });
8805
+ }
8806
+ });
8807
+
8730
8808
  // ../shared/src/config.ts
8731
8809
  var zHanomanConfig;
8732
8810
  var init_config = __esm({
@@ -14003,6 +14081,18 @@ var init_system = __esm({
14003
14081
  build: () => ({ method: "GET", path: "/limits/codex" }),
14004
14082
  shape: (raw) => raw
14005
14083
  },
14084
+ {
14085
+ name: "hanoman_overview_get",
14086
+ title: "Ringkasan kinerja hari ini (Overview)",
14087
+ description: 'Agregat kinerja workspace hari ini vs kemarin, persis yang tampil di layar Overview "Koran Pagi": spec mendarat, sesi agen tumbang, menit agen menunggu jawaban manusia, token agen tercatat (hanya subagen/agen fase), prakiraan kapan limit habis dibanding reset, peta panas jam kerja agen 7\xD724, streak/rekor, dan headline-nya. Menit menunggu & prakiraan limit direkam di memori server dan hilang saat restart (`recordingSince`). Panel `null` = sumbernya gagal dibaca. Tak menuntut capability apa pun.',
14088
+ inputSchema: obj({ properties: {} }),
14089
+ mode: "read",
14090
+ capability: null,
14091
+ samplePath: "/overview",
14092
+ sampleMethod: "GET",
14093
+ build: () => ({ method: "GET", path: "/overview" }),
14094
+ shape: (raw) => raw
14095
+ },
14006
14096
  {
14007
14097
  name: "hanoman_update_status",
14008
14098
  title: "Status versi & pembaruan",
@@ -16543,6 +16633,7 @@ var init_src = __esm({
16543
16633
  init_session_kind();
16544
16634
  init_session_admission();
16545
16635
  init_session_end();
16636
+ init_overview();
16546
16637
  init_config();
16547
16638
  init_config_registry();
16548
16639
  init_semver();
@@ -17427,7 +17518,7 @@ var init_settings2 = __esm({
17427
17518
  const f = `'${decisionFile.split("'").join("'\\''")}'`;
17428
17519
  hooks2.Notification = [{ hooks: [{
17429
17520
  type: "command",
17430
- command: `grep -qiE 'idle|permission|waiting for|needs.?input' && { [ -s ${f} ] || date +%s > ${f}; } || true`
17521
+ command: `grep -qiE 'idle|permission|waiting for|needs.?input' && date +%s > ${f} || true`
17431
17522
  }] }];
17432
17523
  hooks2.UserPromptSubmit = [{ hooks: [{ type: "command", command: `: > ${f}` }] }];
17433
17524
  }
@@ -17444,7 +17535,7 @@ var init_settings2 = __esm({
17444
17535
  (hooks2.Stop ??= []).push({ hooks: [{ type: "command", command: `${busySnapshotCommand(sub)}; exit 0` }] });
17445
17536
  hooks2.Notification = [{ hooks: [{
17446
17537
  type: "command",
17447
- command: `m=$(cat); if [ -s ${sub} ]; then echo "$m" | grep -qiE 'permission|needs.?input'; else echo "$m" | grep -qiE 'idle|permission|waiting for|needs.?input'; fi && { [ -s ${f} ] || date +%s > ${f}; } || true`
17538
+ command: `m=$(cat); if [ -s ${sub} ]; then echo "$m" | grep -qiE 'permission|needs.?input'; else echo "$m" | grep -qiE 'idle|permission|waiting for|needs.?input'; fi && date +%s > ${f} || true`
17448
17539
  }] }];
17449
17540
  hooks2.UserPromptSubmit = [{ hooks: [{ type: "command", command: `: > ${f}; : > ${sub}` }] }];
17450
17541
  }
@@ -17522,7 +17613,7 @@ function codexHookArgs(o) {
17522
17613
  const stop = [];
17523
17614
  const submit = [];
17524
17615
  if (o.decisionFile) {
17525
- stop.push(`[ -s ${shq(o.decisionFile)} ] || date +%s > ${shq(o.decisionFile)}`);
17616
+ stop.push(`date +%s > ${shq(o.decisionFile)}`);
17526
17617
  submit.push(`: > ${shq(o.decisionFile)}`);
17527
17618
  }
17528
17619
  if (o.eventHook) stop.push(EVENT_HOOK_COMMAND);
@@ -20996,7 +21087,7 @@ var require_timers = __commonJS({
20996
21087
  "use strict";
20997
21088
  var fastNow = 0;
20998
21089
  var RESOLUTION_MS = 1e3;
20999
- var TICK_MS5 = (RESOLUTION_MS >> 1) - 1;
21090
+ var TICK_MS6 = (RESOLUTION_MS >> 1) - 1;
21000
21091
  var fastNowTimeout;
21001
21092
  var kFastTimer = Symbol("kFastTimer");
21002
21093
  var fastTimers = [];
@@ -21005,21 +21096,21 @@ var require_timers = __commonJS({
21005
21096
  var PENDING = 0;
21006
21097
  var ACTIVE = 1;
21007
21098
  function onTick() {
21008
- fastNow += TICK_MS5;
21099
+ fastNow += TICK_MS6;
21009
21100
  let idx = 0;
21010
21101
  let len = fastTimers.length;
21011
21102
  while (idx < len) {
21012
- const timer10 = fastTimers[idx];
21013
- if (timer10._state === PENDING) {
21014
- timer10._idleStart = fastNow - TICK_MS5;
21015
- timer10._state = ACTIVE;
21016
- } else if (timer10._state === ACTIVE && fastNow >= timer10._idleStart + timer10._idleTimeout) {
21017
- timer10._state = TO_BE_CLEARED;
21018
- timer10._idleStart = -1;
21019
- timer10._onTimeout(timer10._timerArg);
21020
- }
21021
- if (timer10._state === TO_BE_CLEARED) {
21022
- timer10._state = NOT_IN_LIST;
21103
+ const timer11 = fastTimers[idx];
21104
+ if (timer11._state === PENDING) {
21105
+ timer11._idleStart = fastNow - TICK_MS6;
21106
+ timer11._state = ACTIVE;
21107
+ } else if (timer11._state === ACTIVE && fastNow >= timer11._idleStart + timer11._idleTimeout) {
21108
+ timer11._state = TO_BE_CLEARED;
21109
+ timer11._idleStart = -1;
21110
+ timer11._onTimeout(timer11._timerArg);
21111
+ }
21112
+ if (timer11._state === TO_BE_CLEARED) {
21113
+ timer11._state = NOT_IN_LIST;
21023
21114
  if (--len !== 0) {
21024
21115
  fastTimers[idx] = fastTimers[len];
21025
21116
  }
@@ -21037,7 +21128,7 @@ var require_timers = __commonJS({
21037
21128
  fastNowTimeout.refresh();
21038
21129
  } else {
21039
21130
  clearTimeout(fastNowTimeout);
21040
- fastNowTimeout = setTimeout(onTick, TICK_MS5);
21131
+ fastNowTimeout = setTimeout(onTick, TICK_MS6);
21041
21132
  fastNowTimeout?.unref();
21042
21133
  }
21043
21134
  }
@@ -33764,22 +33855,22 @@ var require_mock_utils = __commonJS({
33764
33855
  return true;
33765
33856
  }
33766
33857
  let aborted = false;
33767
- let timer10 = null;
33858
+ let timer11 = null;
33768
33859
  function abort(err) {
33769
33860
  if (aborted) {
33770
33861
  return;
33771
33862
  }
33772
33863
  aborted = true;
33773
- if (timer10 !== null) {
33774
- clearTimeout(timer10);
33775
- timer10 = null;
33864
+ if (timer11 !== null) {
33865
+ clearTimeout(timer11);
33866
+ timer11 = null;
33776
33867
  }
33777
33868
  handler.onError(err);
33778
33869
  }
33779
33870
  handler.onConnect?.(abort, null);
33780
33871
  if (typeof delay2 === "number" && delay2 > 0) {
33781
- timer10 = setTimeout(() => {
33782
- timer10 = null;
33872
+ timer11 = setTimeout(() => {
33873
+ timer11 = null;
33783
33874
  handleReply(this[kDispatches]);
33784
33875
  }, delay2);
33785
33876
  } else {
@@ -46913,9 +47004,9 @@ async function pinnedRequest(input) {
46913
47004
  });
46914
47005
  response.on("error", reject2);
46915
47006
  });
46916
- const timer10 = setTimeout(() => req.destroy(Object.assign(new Error("outbound timeout"), { name: "AbortError" })), input.totalMs);
46917
- timer10.unref?.();
46918
- req.once("close", () => clearTimeout(timer10));
47007
+ const timer11 = setTimeout(() => req.destroy(Object.assign(new Error("outbound timeout"), { name: "AbortError" })), input.totalMs);
47008
+ timer11.unref?.();
47009
+ req.once("close", () => clearTimeout(timer11));
46919
47010
  req.once("error", reject2);
46920
47011
  if (input.body) req.write(input.body);
46921
47012
  req.end();
@@ -46923,13 +47014,13 @@ async function pinnedRequest(input) {
46923
47014
  }
46924
47015
  function withLookupTimeout(p3, ms) {
46925
47016
  return new Promise((resolve24, reject2) => {
46926
- const timer10 = setTimeout(() => reject2(Object.assign(new Error("outbound timeout"), { name: "AbortError" })), ms);
46927
- timer10.unref?.();
47017
+ const timer11 = setTimeout(() => reject2(Object.assign(new Error("outbound timeout"), { name: "AbortError" })), ms);
47018
+ timer11.unref?.();
46928
47019
  p3.then((v) => {
46929
- clearTimeout(timer10);
47020
+ clearTimeout(timer11);
46930
47021
  resolve24(v);
46931
47022
  }, (e) => {
46932
- clearTimeout(timer10);
47023
+ clearTimeout(timer11);
46933
47024
  reject2(e);
46934
47025
  });
46935
47026
  });
@@ -47041,19 +47132,19 @@ async function dropSessionUploads(sessionId2) {
47041
47132
  });
47042
47133
  }
47043
47134
  async function readUpload(storageKey) {
47044
- const safe2 = storageKey.replace(/[/\\]/g, "");
47045
- return readFile2(join15(uploadDir(), safe2));
47135
+ const safe3 = storageKey.replace(/[/\\]/g, "");
47136
+ return readFile2(join15(uploadDir(), safe3));
47046
47137
  }
47047
47138
  async function readUploadOrFetch(storageKey) {
47048
- const safe2 = storageKey.replace(/[/\\]/g, "");
47049
- const target3 = join15(uploadDir(), safe2);
47139
+ const safe3 = storageKey.replace(/[/\\]/g, "");
47140
+ const target3 = join15(uploadDir(), safe3);
47050
47141
  try {
47051
47142
  return await readFile2(target3);
47052
47143
  } catch {
47053
47144
  const base3 = effectiveStr("SYNC_SERVER_URL");
47054
47145
  const token = effectiveStr("SYNC_DEVICE_TOKEN");
47055
- if (!base3 || !token) throw new Error(`lampiran ${safe2} tak ada lokal & bukan client sync`);
47056
- const url2 = new URL(`/api/sync/attachments/${encodeURIComponent(safe2)}`, `${base3.replace(/\/$/, "")}/`);
47146
+ if (!base3 || !token) throw new Error(`lampiran ${safe3} tak ada lokal & bukan client sync`);
47147
+ const url2 = new URL(`/api/sync/attachments/${encodeURIComponent(safe3)}`, `${base3.replace(/\/$/, "")}/`);
47057
47148
  const loopback = ["localhost", "127.0.0.1", "::1"].includes(url2.hostname);
47058
47149
  const res = await safeRequest({
47059
47150
  url: url2,
@@ -47072,8 +47163,8 @@ async function readUploadOrFetch(storageKey) {
47072
47163
  }
47073
47164
  }
47074
47165
  async function deleteUpload(storageKey) {
47075
- const safe2 = storageKey.replace(/[/\\]/g, "");
47076
- await unlink(join15(uploadDir(), safe2)).catch(() => {
47166
+ const safe3 = storageKey.replace(/[/\\]/g, "");
47167
+ await unlink(join15(uploadDir(), safe3)).catch(() => {
47077
47168
  });
47078
47169
  }
47079
47170
  var EXT, SESSION_ID;
@@ -47713,7 +47804,7 @@ function parsePanes(out4) {
47713
47804
  // `UserPromptSubmit`, jadi ia tetap terisi sepanjang agen bekerja sesudah pertanyaannya
47714
47805
  // dijawab lewat jalur lain (dialog di TUI, route SPEC-899, Esc, codex yang melanjutkan
47715
47806
  // sendiri). Pane yang masih mengeluarkan sesuatu berarti agen sudah lanjut bekerja.
47716
- decision: !exited && !!decisionFile && markerFilled(decisionFile) && paneQuiet(activityAt),
47807
+ decision: !exited && !!decisionFile && markerFilled(decisionFile) && paneQuiet(activityAt) && markerCurrent(decisionFile, activityAt),
47717
47808
  // SPEC-338 · sesi yang lahir sebelum ADR-0074 tak punya opsi ini → claude.
47718
47809
  agent: agent === "codex" ? "codex" : "claude",
47719
47810
  altScreen: alternate === "1",
@@ -48459,7 +48550,7 @@ function spawnPty(...args) {
48459
48550
  );
48460
48551
  }
48461
48552
  }
48462
- var socket, PREFIX, MAX_SCROLLBACK, SCROLLBACK_SLACK, POLL_MS, markerFilled, markerFilledAsync, clearMarker, PANE_QUIET_MS, paneQuiet, markerOnset, decisionOnset, attached, phaseInvocations, PHASE_DONE_SEEN_OPT, phaseDoneSeen, claudeBin, shellBin, codexBin, agentBin, rootBypassEnv, frame, name, promptFilePath, goalGatePath, goalStatePath, agentTempDir, agentsFilePath, phaseContextFilePath, AGENTS_ARG_SAFE_BYTES, agentContractFilePath, askpassDenyPath, ASKPASS_DENY, SUBAGENT_CONCURRENCY_CAP, NO_SERVER, TmuxError, sq, idFor, FMT, toSessionInfo, listSessions, listSessionsAsync, decisionsOf, getSession, getSessionAsync, hooks, emitBirth, emitDeath, customAgentSource, codexNativeAgentSupport, nativeAgentsAvailable, customAgentsFor, runGitDiff, sleep, paneText, paneIO, DIALOG_CAPTURE_LINES, GOAL_ARMED_MARKERS, goalArmed, COALESCE_MS, COALESCE_MAX_BYTES, trimScrollback, phaseKey, sessionFinishedAsync, poll, polling, TERMINAL_QUERY, stripTerminalQueries, CAPTURE_ARGS, detach;
48553
+ var socket, PREFIX, MAX_SCROLLBACK, SCROLLBACK_SLACK, POLL_MS, markerFilled, markerFilledAsync, clearMarker, PANE_QUIET_MS, paneQuiet, MARKER_STALE_SLACK_S, markerOnset, markerCurrent, decisionOnset, attached, phaseInvocations, PHASE_DONE_SEEN_OPT, phaseDoneSeen, claudeBin, shellBin, codexBin, agentBin, rootBypassEnv, frame, name, promptFilePath, goalGatePath, goalStatePath, agentTempDir, agentsFilePath, phaseContextFilePath, AGENTS_ARG_SAFE_BYTES, agentContractFilePath, askpassDenyPath, ASKPASS_DENY, SUBAGENT_CONCURRENCY_CAP, NO_SERVER, TmuxError, sq, idFor, FMT, toSessionInfo, listSessions, listSessionsAsync, decisionsOf, getSession, getSessionAsync, hooks, emitBirth, emitDeath, customAgentSource, codexNativeAgentSupport, nativeAgentsAvailable, customAgentsFor, runGitDiff, sleep, paneText, paneIO, DIALOG_CAPTURE_LINES, GOAL_ARMED_MARKERS, goalArmed, COALESCE_MS, COALESCE_MAX_BYTES, trimScrollback, phaseKey, sessionFinishedAsync, poll, polling, TERMINAL_QUERY, stripTerminalQueries, CAPTURE_ARGS, detach;
48463
48554
  var init_pty = __esm({
48464
48555
  "src/services/pty.ts"() {
48465
48556
  "use strict";
@@ -48505,6 +48596,7 @@ var init_pty = __esm({
48505
48596
  };
48506
48597
  PANE_QUIET_MS = 3e3;
48507
48598
  paneQuiet = (activityAt, now = Date.now()) => !(activityAt > 0) || now - activityAt * 1e3 >= PANE_QUIET_MS;
48599
+ MARKER_STALE_SLACK_S = 10;
48508
48600
  markerOnset = (f) => {
48509
48601
  let raw;
48510
48602
  try {
@@ -48515,6 +48607,10 @@ var init_pty = __esm({
48515
48607
  const secs = Number(raw.trim());
48516
48608
  return Number.isInteger(secs) && secs > 0 ? secs : 0;
48517
48609
  };
48610
+ markerCurrent = (f, activityAt) => {
48611
+ const onset = markerOnset(f);
48612
+ return onset === 0 || !(activityAt > 0) || activityAt <= onset + MARKER_STALE_SLACK_S;
48613
+ };
48518
48614
  decisionOnset = (f, activityAt) => {
48519
48615
  const secs = Math.max(markerOnset(f), activityAt > 0 ? Math.floor(activityAt) : 0);
48520
48616
  return secs > 0 ? new Date(secs * 1e3).toISOString() : void 0;
@@ -54168,9 +54264,9 @@ function recordBytes(rec) {
54168
54264
  return Buffer.byteLength(JSON.stringify(rec));
54169
54265
  }
54170
54266
  async function pull(sinceCursor, limit = 500, maxBytes = PULL_MAX_BYTES, opts2 = {}) {
54171
- const since = Number(sinceCursor || "0");
54267
+ const since2 = Number(sinceCursor || "0");
54172
54268
  const rows = await prisma.syncLog.findMany({
54173
- where: { seq: { gt: since } },
54269
+ where: { seq: { gt: since2 } },
54174
54270
  orderBy: { seq: "asc" },
54175
54271
  take: limit
54176
54272
  });
@@ -55361,12 +55457,12 @@ function requestRelay(deviceId, r, opts2 = {}) {
55361
55457
  return Promise.reject(new RelayError("too-large"));
55362
55458
  const id2 = `r${++counter}`;
55363
55459
  return new Promise((resolve24, reject2) => {
55364
- const timer10 = setTimeout(() => {
55460
+ const timer11 = setTimeout(() => {
55365
55461
  sendCancel(link2, id2);
55366
55462
  settle(link2, id2, new RelayError("timeout"));
55367
55463
  }, opts2.timeoutMs ?? RELAY_REQ_TIMEOUT_MS);
55368
- timer10.unref?.();
55369
- link2.pending.set(id2, { resolve: resolve24, reject: reject2, timer: timer10, contentType: null, chunks: [], bytes: 0 });
55464
+ timer11.unref?.();
55465
+ link2.pending.set(id2, { resolve: resolve24, reject: reject2, timer: timer11, contentType: null, chunks: [], bytes: 0 });
55370
55466
  try {
55371
55467
  link2.socket.send(JSON.stringify({
55372
55468
  t: "req",
@@ -56689,8 +56785,8 @@ var init_store = __esm({
56689
56785
  async bindSession(chatId, sessionId2) {
56690
56786
  await this.db.telegramChat.update({ where: { chatId }, data: { sessionId: sessionId2 } });
56691
56787
  }
56692
- async rateLimitExceeded(userId, since, limit) {
56693
- return await this.db.telegramUpdate.count({ where: { userId, receivedAt: { gte: since } } }) > limit;
56788
+ async rateLimitExceeded(userId, since2, limit) {
56789
+ return await this.db.telegramUpdate.count({ where: { userId, receivedAt: { gte: since2 } } }) > limit;
56694
56790
  }
56695
56791
  async markUpdateUncertain(updateId, reason) {
56696
56792
  await this.db.telegramUpdate.updateMany({
@@ -56709,9 +56805,9 @@ var init_store = __esm({
56709
56805
  * SELAMANYA, dan tanpa pagar ini gateway akan mengetik selamanya sekaligus mengunci long-poll
56710
56806
  * di 4 detik selamanya.
56711
56807
  */
56712
- async chatsAwaitingReply(since) {
56808
+ async chatsAwaitingReply(since2) {
56713
56809
  const pending2 = await this.db.telegramUpdate.findMany({
56714
- where: { state: "dispatched", chatId: { not: null }, dispatchedAt: { gte: since } },
56810
+ where: { state: "dispatched", chatId: { not: null }, dispatchedAt: { gte: since2 } },
56715
56811
  select: { updateId: true, chatId: true },
56716
56812
  orderBy: { updateId: "asc" }
56717
56813
  });
@@ -57318,12 +57414,12 @@ function startPresenceSender(o) {
57318
57414
  heartbeatMs: o.heartbeatMs
57319
57415
  });
57320
57416
  void sender.tick(Date.now());
57321
- const timer10 = setInterval(() => {
57417
+ const timer11 = setInterval(() => {
57322
57418
  void sender.tick(Date.now());
57323
57419
  }, o.tickMs ?? PRESENCE_TICK_MS);
57324
- timer10.unref?.();
57420
+ timer11.unref?.();
57325
57421
  return { stop() {
57326
- clearInterval(timer10);
57422
+ clearInterval(timer11);
57327
57423
  } };
57328
57424
  }
57329
57425
  var init_sender = __esm({
@@ -57374,7 +57470,7 @@ function capabilityForRoute(method, path) {
57374
57470
  if (top === "devices") return "relay:exec";
57375
57471
  if (top === "logs") return rw("logs");
57376
57472
  if (top === "presence") return read2 ? "presence:read" : "COOKIE_ONLY";
57377
- if (top === "limits" || top === "update" || top === "events" || top === "fs" || top === "health" || top === "models" || top === "methods" || top === "codex")
57473
+ if (top === "limits" || top === "update" || top === "events" || top === "fs" || top === "health" || top === "models" || top === "methods" || top === "codex" || top === "overview")
57378
57474
  return read2 ? "GLOBAL_READ" : "COOKIE_ONLY";
57379
57475
  if (top === "session-results") return rw("activity");
57380
57476
  if (top === "portal-chat") return rw("portal-chat");
@@ -60991,7 +61087,7 @@ var require_stream = __commonJS({
60991
61087
  };
60992
61088
  duplex._final = function(callback) {
60993
61089
  if (ws2.readyState === ws2.CONNECTING) {
60994
- ws2.once("open", function open5() {
61090
+ ws2.once("open", function open6() {
60995
61091
  duplex._final(callback);
60996
61092
  });
60997
61093
  return;
@@ -61012,7 +61108,7 @@ var require_stream = __commonJS({
61012
61108
  };
61013
61109
  duplex._write = function(chunk, encoding, callback) {
61014
61110
  if (ws2.readyState === ws2.CONNECTING) {
61015
- ws2.once("open", function open5() {
61111
+ ws2.once("open", function open6() {
61016
61112
  duplex._write(chunk, encoding, callback);
61017
61113
  });
61018
61114
  return;
@@ -61744,14 +61840,14 @@ async function loadCursor(lane) {
61744
61840
  const c = await prisma.logCursor.findUnique({ where: { deviceId_lane: { deviceId: LOCAL_DEVICE_ID, lane } } });
61745
61841
  return c?.seq ?? 0n;
61746
61842
  }
61747
- async function nextBatch(lane, since) {
61843
+ async function nextBatch(lane, since2) {
61748
61844
  if (lane === "server") {
61749
61845
  const segs = await readSpoolSegments("server");
61750
61846
  const flat = segs.flatMap((s2) => s2.entries);
61751
61847
  return flat.slice(0, LOG_BATCH_MAX_ENTRIES);
61752
61848
  }
61753
61849
  const rows = await prisma.logEntry.findMany({
61754
- where: { deviceId: LOCAL_DEVICE_ID, lane, seq: { gt: since } },
61850
+ where: { deviceId: LOCAL_DEVICE_ID, lane, seq: { gt: since2 } },
61755
61851
  orderBy: { seq: "asc" },
61756
61852
  take: LOG_BATCH_MAX_ENTRIES
61757
61853
  });
@@ -61770,9 +61866,9 @@ async function nextBatch(lane, since) {
61770
61866
  async function shipLane(lane, transport) {
61771
61867
  const now = Date.now();
61772
61868
  if ((pausedUntil.get(lane) ?? 0) > now) return;
61773
- let since = await loadCursor(lane);
61869
+ let since2 = await loadCursor(lane);
61774
61870
  for (let i = 0; i < LOG_SHIP_MAX_BATCHES_PER_TICK; i++) {
61775
- const entries4 = await nextBatch(lane, since);
61871
+ const entries4 = await nextBatch(lane, since2);
61776
61872
  if (entries4.length === 0) return;
61777
61873
  const res = await transport("POST", "/api/sync/logs", { lane, entries: entries4 });
61778
61874
  if (res.status === 200) {
@@ -61783,7 +61879,7 @@ async function shipLane(lane, transport) {
61783
61879
  update: { seq: last2 },
61784
61880
  create: { deviceId: LOCAL_DEVICE_ID, lane, seq: last2 }
61785
61881
  });
61786
- since = last2;
61882
+ since2 = last2;
61787
61883
  if (lane === "server") {
61788
61884
  const segs = await readSpoolSegments("server");
61789
61885
  for (const s2 of segs) if (s2.entries.every((e) => BigInt(e.seq) <= last2)) await removeSpoolSegment(s2.file);
@@ -61792,7 +61888,7 @@ async function shipLane(lane, transport) {
61792
61888
  }
61793
61889
  if (res.status === 400) {
61794
61890
  await appendGap("rejected");
61795
- since = BigInt(entries4[entries4.length - 1].seq);
61891
+ since2 = BigInt(entries4[entries4.length - 1].seq);
61796
61892
  continue;
61797
61893
  }
61798
61894
  if (res.status === 404) {
@@ -62241,7 +62337,7 @@ async function startSyncClient(base3, token, tickMs2) {
62241
62337
  started = true;
62242
62338
  startRelayClient(base3, token);
62243
62339
  const transport = fetchTransport(base3, token);
62244
- const tick6 = () => syncTick(transport);
62340
+ const tick7 = () => syncTick(transport);
62245
62341
  const connectWs = async () => {
62246
62342
  const { WebSocket: WebSocket2 } = await Promise.resolve().then(() => (init_wrapper(), wrapper_exports));
62247
62343
  const wsUrl = base3.replace(/^http/, "ws").replace(/\/$/, "") + "/api/sync/ws?" + OPTIONAL_QUERY;
@@ -62249,14 +62345,14 @@ async function startSyncClient(base3, token, tickMs2) {
62249
62345
  ws = sock2;
62250
62346
  sock2.on("open", () => {
62251
62347
  reconnectDelay = 0;
62252
- void tick6();
62348
+ void tick7();
62253
62349
  presence = startPresenceSender({ send: (json) => sock2.send(json) });
62254
62350
  });
62255
62351
  sock2.on("message", async (raw) => {
62256
62352
  try {
62257
62353
  const msg = JSON.parse(raw.toString());
62258
62354
  if (msg.t !== "sync") return;
62259
- if (!await applyFeedFrame(msg)) void tick6();
62355
+ if (!await applyFeedFrame(msg)) void tick7();
62260
62356
  } catch {
62261
62357
  }
62262
62358
  });
@@ -62278,11 +62374,11 @@ async function startSyncClient(base3, token, tickMs2) {
62278
62374
  });
62279
62375
  };
62280
62376
  await bootstrapOnce(transport).catch(() => null);
62281
- await tick6();
62377
+ await tick7();
62282
62378
  void connectWs();
62283
62379
  const ms = tickMs2 && tickMs2 > 0 ? tickMs2 : 15e3;
62284
62380
  timer3 = setInterval(() => {
62285
- void tick6();
62381
+ void tick7();
62286
62382
  }, ms);
62287
62383
  timer3.unref?.();
62288
62384
  }
@@ -65492,14 +65588,14 @@ var require_secure_json_parse = __commonJS({
65492
65588
  }
65493
65589
  return filter(obj2, { protoAction, constructorAction, safe: options2 && options2.safe });
65494
65590
  }
65495
- function filter(obj2, { protoAction = "error", constructorAction = "error", safe: safe2 } = {}) {
65591
+ function filter(obj2, { protoAction = "error", constructorAction = "error", safe: safe3 } = {}) {
65496
65592
  let next = [obj2];
65497
65593
  while (next.length) {
65498
65594
  const nodes = next;
65499
65595
  next = [];
65500
65596
  for (const node of nodes) {
65501
65597
  if (protoAction !== "ignore" && Object.prototype.hasOwnProperty.call(node, "__proto__")) {
65502
- if (safe2 === true) {
65598
+ if (safe3 === true) {
65503
65599
  return null;
65504
65600
  } else if (protoAction === "error") {
65505
65601
  throw new SyntaxError("Object contains forbidden prototype property");
@@ -65507,7 +65603,7 @@ var require_secure_json_parse = __commonJS({
65507
65603
  delete node.__proto__;
65508
65604
  }
65509
65605
  if (constructorAction !== "ignore" && Object.prototype.hasOwnProperty.call(node, "constructor") && node.constructor !== null && typeof node.constructor === "object" && Object.prototype.hasOwnProperty.call(node.constructor, "prototype")) {
65510
- if (safe2 === true) {
65606
+ if (safe3 === true) {
65511
65607
  return null;
65512
65608
  } else if (constructorAction === "error") {
65513
65609
  throw new SyntaxError("Object contains forbidden prototype property");
@@ -66142,12 +66238,12 @@ var UploadError = class extends Error {
66142
66238
  };
66143
66239
  function timeout(promise, ms, code) {
66144
66240
  return new Promise((resolve24, reject2) => {
66145
- const timer10 = setTimeout(() => reject2(new UploadError(code, "upload operation timed out")), ms);
66241
+ const timer11 = setTimeout(() => reject2(new UploadError(code, "upload operation timed out")), ms);
66146
66242
  promise.then((value) => {
66147
- clearTimeout(timer10);
66243
+ clearTimeout(timer11);
66148
66244
  resolve24(value);
66149
66245
  }, (error) => {
66150
- clearTimeout(timer10);
66246
+ clearTimeout(timer11);
66151
66247
  reject2(error);
66152
66248
  });
66153
66249
  });
@@ -66170,16 +66266,16 @@ function scannerFromEnv(path) {
66170
66266
  if (!isAbsolute8(command)) return Promise.reject(new UploadError("UPLOAD_SCAN", "scanner path must be absolute"));
66171
66267
  return new Promise((resolve24, reject2) => {
66172
66268
  const child = spawn2(command, [path], { shell: false, stdio: "ignore" });
66173
- const timer10 = setTimeout(() => {
66269
+ const timer11 = setTimeout(() => {
66174
66270
  child.kill("SIGKILL");
66175
66271
  reject2(new UploadError("UPLOAD_SCAN", "scanner timeout"));
66176
66272
  }, UPLOAD_LIMITS.scanMs);
66177
66273
  child.once("error", (error) => {
66178
- clearTimeout(timer10);
66274
+ clearTimeout(timer11);
66179
66275
  reject2(error);
66180
66276
  });
66181
66277
  child.once("exit", (code) => {
66182
- clearTimeout(timer10);
66278
+ clearTimeout(timer11);
66183
66279
  if (code === 0) resolve24();
66184
66280
  else reject2(new UploadError("UPLOAD_SCAN", `scanner exit ${code}`));
66185
66281
  });
@@ -66335,12 +66431,12 @@ async function addSpecAttachments(spec, files2) {
66335
66431
  continue;
66336
66432
  }
66337
66433
  const ext = attachmentExt(name2);
66338
- let safe2;
66434
+ let safe3;
66339
66435
  try {
66340
66436
  const image = IMAGE_TYPES[f.mime];
66341
66437
  if (image) {
66342
66438
  if (!image.includes(ext)) throw new UploadError("UPLOAD_TYPE", "extension mismatch");
66343
- safe2 = await processUpload({
66439
+ safe3 = await processUpload({
66344
66440
  buffer: f.buf,
66345
66441
  clientName: name2,
66346
66442
  clientMime: f.mime,
@@ -66348,7 +66444,7 @@ async function addSpecAttachments(spec, files2) {
66348
66444
  parentBytes: bytes
66349
66445
  });
66350
66446
  } else if (DOCUMENT_TYPES[f.mime]) {
66351
- safe2 = await processDocumentUpload({
66447
+ safe3 = await processDocumentUpload({
66352
66448
  buffer: f.buf,
66353
66449
  clientName: name2,
66354
66450
  clientMime: f.mime,
@@ -66367,17 +66463,17 @@ async function addSpecAttachments(spec, files2) {
66367
66463
  row = await prisma.specAttachment.create({ data: {
66368
66464
  specId: spec.id,
66369
66465
  projectId: spec.projectId,
66370
- filename: safe2.filename,
66371
- mimeType: safe2.mimeType,
66372
- size: safe2.size,
66373
- storageKey: safe2.storageKey
66466
+ filename: safe3.filename,
66467
+ mimeType: safe3.mimeType,
66468
+ size: safe3.size,
66469
+ storageKey: safe3.storageKey
66374
66470
  } });
66375
66471
  } catch (error) {
66376
- await deleteUpload(safe2.storageKey);
66472
+ await deleteUpload(safe3.storageKey);
66377
66473
  throw error;
66378
66474
  }
66379
66475
  count += 1;
66380
- bytes += safe2.size;
66476
+ bytes += safe3.size;
66381
66477
  saved.push(view2(row));
66382
66478
  }
66383
66479
  return { saved, rejected };
@@ -66585,14 +66681,14 @@ async function addQaAttachments(report, owner, files2) {
66585
66681
  continue;
66586
66682
  }
66587
66683
  const ext = attachmentExt(name2);
66588
- let safe2;
66684
+ let safe3;
66589
66685
  try {
66590
66686
  const image = IMAGE_TYPES[f.mime];
66591
66687
  if (image) {
66592
66688
  if (!image.includes(ext)) throw new UploadError("UPLOAD_TYPE", "extension mismatch");
66593
- safe2 = await processUpload({ buffer: f.buf, clientName: name2, clientMime: f.mime, projectId: report.projectId, parentBytes: bytes });
66689
+ safe3 = await processUpload({ buffer: f.buf, clientName: name2, clientMime: f.mime, projectId: report.projectId, parentBytes: bytes });
66594
66690
  } else if (DOCUMENT_TYPES[f.mime]) {
66595
- safe2 = await processDocumentUpload({ buffer: f.buf, clientName: name2, clientMime: f.mime, clientExt: ext });
66691
+ safe3 = await processDocumentUpload({ buffer: f.buf, clientName: name2, clientMime: f.mime, clientExt: ext });
66596
66692
  } else {
66597
66693
  throw new UploadError("UPLOAD_TYPE", "unsupported type");
66598
66694
  }
@@ -66602,28 +66698,28 @@ async function addQaAttachments(report, owner, files2) {
66602
66698
  continue;
66603
66699
  }
66604
66700
  try {
66605
- const sha256 = createHash6("sha256").update(await readUpload(safe2.storageKey)).digest("hex");
66701
+ const sha256 = createHash6("sha256").update(await readUpload(safe3.storageKey)).digest("hex");
66606
66702
  const row = await prisma.qaAttachment.create({ data: {
66607
66703
  reportId: report.id,
66608
66704
  projectId: report.projectId,
66609
66705
  ownerType: owner.ownerType,
66610
66706
  ownerId: owner.ownerId,
66611
- filename: safe2.filename,
66612
- mimeType: safe2.mimeType,
66613
- size: safe2.size,
66707
+ filename: safe3.filename,
66708
+ mimeType: safe3.mimeType,
66709
+ size: safe3.size,
66614
66710
  sha256,
66615
- storageKey: safe2.storageKey,
66711
+ storageKey: safe3.storageKey,
66616
66712
  // Client: byte baru ada di sini saja → menunggu unggah ke hub. Hub/standalone: tak ada atasan → langsung tersedia.
66617
66713
  syncState: effectiveStr("SYNC_SERVER_URL") ? "local-only" : "available"
66618
66714
  } });
66619
66715
  await notifySynced("qaAttachment", row.id);
66620
66716
  saved.push(attachmentView(row));
66621
66717
  } catch (error) {
66622
- await deleteUpload(safe2.storageKey);
66718
+ await deleteUpload(safe3.storageKey);
66623
66719
  throw error;
66624
66720
  }
66625
66721
  count += 1;
66626
- bytes += safe2.size;
66722
+ bytes += safe3.size;
66627
66723
  }
66628
66724
  return { saved, rejected };
66629
66725
  }
@@ -66959,9 +67055,9 @@ async function toProjectView(p3, sessions, devices2) {
66959
67055
  const specs = await prisma.spec.findMany({ where: { projectId: p3.id } });
66960
67056
  const { coverage } = await scanRepoDocs(await resolveRepoDir(p3.id));
66961
67057
  const binding = await getBinding(p3.id);
66962
- const open5 = specs.filter((s2) => s2.stage !== "done");
67058
+ const open6 = specs.filter((s2) => s2.stage !== "done");
66963
67059
  const { session, commit } = sessionOf(p3.id, sessions);
66964
- const topStage = open5.length ? open5.map((s2) => s2.stage).sort((a, b) => STAGES.indexOf(b) - STAGES.indexOf(a))[0] : "spec";
67060
+ const topStage = open6.length ? open6.map((s2) => s2.stage).sort((a, b) => STAGES.indexOf(b) - STAGES.indexOf(a))[0] : "spec";
66965
67061
  return {
66966
67062
  id: p3.id,
66967
67063
  name: p3.name,
@@ -66974,7 +67070,7 @@ async function toProjectView(p3, sessions, devices2) {
66974
67070
  docStatus: docStatusFor(coverage),
66975
67071
  coverage,
66976
67072
  createdAt: p3.createdAt.toISOString(),
66977
- backlog: open5.length,
67073
+ backlog: open6.length,
66978
67074
  topStage,
66979
67075
  session,
66980
67076
  activity: session.status === "running" ? `running \xB7 ${session.flow ?? "sesi"}` : "idle",
@@ -68443,9 +68539,9 @@ async function deleteWorktrees(repoDir, names, opts2) {
68443
68539
  try {
68444
68540
  if (w.locked) await opts2.unlock(report.repoDir, w.path);
68445
68541
  if (w.session) {
68446
- const closed = await opts2.closeSession(w.session.id);
68542
+ const closed2 = await opts2.closeSession(w.session.id);
68447
68543
  row.closedSession = w.session.id;
68448
- row.cleanup = closed?.cleanup ?? null;
68544
+ row.cleanup = closed2?.cleanup ?? null;
68449
68545
  }
68450
68546
  if (!row.cleanup) row.cleanup = opts2.release(report.repoDir, w.path);
68451
68547
  await opts2.prune(report.repoDir);
@@ -68752,11 +68848,11 @@ async function beginSession(b) {
68752
68848
  });
68753
68849
  }
68754
68850
  async function finishSession(d) {
68755
- const open5 = await prisma.sessionHistory.findFirst({
68851
+ const open6 = await prisma.sessionHistory.findFirst({
68756
68852
  where: { sessionId: d.sessionId, endedAt: null },
68757
68853
  orderBy: { startedAt: "desc" }
68758
68854
  });
68759
- if (!open5) return;
68855
+ if (!open6) return;
68760
68856
  let t = { key: "", bytes: 0 };
68761
68857
  if (d.transcript) {
68762
68858
  try {
@@ -68766,7 +68862,7 @@ async function finishSession(d) {
68766
68862
  }
68767
68863
  }
68768
68864
  await prisma.sessionHistory.update({
68769
- where: { id: open5.id },
68865
+ where: { id: open6.id },
68770
68866
  data: {
68771
68867
  endedAt: /* @__PURE__ */ new Date(),
68772
68868
  endedReason: CLOSED,
@@ -68881,22 +68977,22 @@ async function reconcileTranscripts(opts2 = {}) {
68881
68977
  return report;
68882
68978
  }
68883
68979
  async function reconcileHistory(liveSessionIds) {
68884
- const open5 = await prisma.sessionHistory.findMany({
68980
+ const open6 = await prisma.sessionHistory.findMany({
68885
68981
  where: { endedAt: null },
68886
68982
  select: { id: true, sessionId: true, updatedAt: true }
68887
68983
  });
68888
68984
  const live = new Set(liveSessionIds);
68889
68985
  const at = /* @__PURE__ */ new Date();
68890
- let closed = 0;
68891
- for (const r of open5) {
68986
+ let closed2 = 0;
68987
+ for (const r of open6) {
68892
68988
  if (live.has(r.sessionId)) continue;
68893
68989
  await prisma.sessionHistory.update({
68894
68990
  where: { id: r.id },
68895
68991
  data: { endedAt: r.updatedAt, endedReason: RECONCILED, reconciledAt: at }
68896
68992
  });
68897
- closed++;
68993
+ closed2++;
68898
68994
  }
68899
- return closed;
68995
+ return closed2;
68900
68996
  }
68901
68997
  async function reconciledSpecIdsSince(cutoff) {
68902
68998
  const rows = await prisma.sessionHistory.findMany({
@@ -68968,13 +69064,13 @@ async function detectOrphanWorktrees(deps = { repos: prodReaperDeps.repos, input
68968
69064
  // src/services/worktree-delete.ts
68969
69065
  var execAsync = promisify11(execFile12);
68970
69066
  async function lockInputs(id2) {
68971
- const open5 = await prisma.spec.findMany({
69067
+ const open6 = await prisma.spec.findMany({
68972
69068
  where: { projectId: id2, stage: { not: "done" } },
68973
69069
  select: { id: true }
68974
69070
  });
68975
69071
  const sessions = listSessions().filter((s2) => s2.projectId === id2 && !s2.exited).map((s2) => s2.branch || (s2.specId ? `hanoman/${s2.id}` : "")).filter(Boolean);
68976
69072
  return {
68977
- openSpecBranches: new Set(open5.map((s2) => sourceBranch(s2.id))),
69073
+ openSpecBranches: new Set(open6.map((s2) => sourceBranch(s2.id))),
68978
69074
  sessionBranches: new Set(sessions)
68979
69075
  };
68980
69076
  }
@@ -74241,7 +74337,7 @@ function createBoundedSender(ws2, o = {}) {
74241
74337
  const drainMs = o.drainMs ?? DRAIN_MS;
74242
74338
  let pending2 = [];
74243
74339
  let pendingBytes = 0;
74244
- let timer10;
74340
+ let timer11;
74245
74341
  let resyncNeeded = false;
74246
74342
  const trim = () => {
74247
74343
  for (let i = 0; i < pending2.length && pendingBytes > cap; ) {
@@ -74255,10 +74351,10 @@ function createBoundedSender(ws2, o = {}) {
74255
74351
  }
74256
74352
  };
74257
74353
  const drain2 = () => {
74258
- timer10 = void 0;
74354
+ timer11 = void 0;
74259
74355
  if (ws2.bufferedAmount > high) {
74260
- timer10 = setTimeout(drain2, drainMs);
74261
- timer10.unref?.();
74356
+ timer11 = setTimeout(drain2, drainMs);
74357
+ timer11.unref?.();
74262
74358
  return;
74263
74359
  }
74264
74360
  const out4 = pending2;
@@ -74279,14 +74375,14 @@ function createBoundedSender(ws2, o = {}) {
74279
74375
  pending2.push(frame2);
74280
74376
  pendingBytes += frame2.length;
74281
74377
  trim();
74282
- if (!timer10) {
74283
- timer10 = setTimeout(drain2, drainMs);
74284
- timer10.unref?.();
74378
+ if (!timer11) {
74379
+ timer11 = setTimeout(drain2, drainMs);
74380
+ timer11.unref?.();
74285
74381
  }
74286
74382
  },
74287
74383
  dispose() {
74288
- if (timer10) clearTimeout(timer10);
74289
- timer10 = void 0;
74384
+ if (timer11) clearTimeout(timer11);
74385
+ timer11 = void 0;
74290
74386
  pending2 = [];
74291
74387
  pendingBytes = 0;
74292
74388
  }
@@ -74434,7 +74530,7 @@ function startSessionEventRelay(app3, options2 = {}) {
74434
74530
  const root = options2.root ?? sessionEventSpoolRoot();
74435
74531
  const host2 = controlHost(loadIngressPolicy(options2.env ?? process.env));
74436
74532
  let running = false;
74437
- const tick6 = async () => {
74533
+ const tick7 = async () => {
74438
74534
  if (running) return;
74439
74535
  running = true;
74440
74536
  try {
@@ -74445,14 +74541,14 @@ function startSessionEventRelay(app3, options2 = {}) {
74445
74541
  running = false;
74446
74542
  }
74447
74543
  };
74448
- const timer10 = setInterval(() => {
74449
- void tick6();
74544
+ const timer11 = setInterval(() => {
74545
+ void tick7();
74450
74546
  }, options2.intervalMs ?? 250);
74451
- timer10.unref();
74547
+ timer11.unref();
74452
74548
  app3.addHook("onClose", async () => {
74453
- clearInterval(timer10);
74549
+ clearInterval(timer11);
74454
74550
  });
74455
- void tick6();
74551
+ void tick7();
74456
74552
  }
74457
74553
 
74458
74554
  // src/services/agent-invocations.ts
@@ -74676,10 +74772,10 @@ async function stopAgentInvocation(input, io = {}) {
74676
74772
  }
74677
74773
  async function reconcileAgentInvocations(liveSessionIds) {
74678
74774
  const live = new Set(liveSessionIds);
74679
- const open5 = await prisma.agentInvocation.findMany({ where: { status: "running" } });
74775
+ const open6 = await prisma.agentInvocation.findMany({ where: { status: "running" } });
74680
74776
  let changed = 0;
74681
74777
  const endedAt = /* @__PURE__ */ new Date();
74682
- for (const row of open5) {
74778
+ for (const row of open6) {
74683
74779
  if (live.has(row.sessionId)) continue;
74684
74780
  await prisma.agentInvocation.update({
74685
74781
  where: { id: row.id },
@@ -75075,8 +75171,8 @@ async function expireFlows(now) {
75075
75171
  });
75076
75172
  const out4 = [];
75077
75173
  for (const f of due) {
75078
- const closed = await closeFlow(f.id, "kedaluwarsa");
75079
- if (closed) out4.push(closed);
75174
+ const closed2 = await closeFlow(f.id, "kedaluwarsa");
75175
+ if (closed2) out4.push(closed2);
75080
75176
  }
75081
75177
  return out4;
75082
75178
  }
@@ -76763,7 +76859,7 @@ async function terminal_default(app3, opts2) {
76763
76859
  });
76764
76860
  const client = { send: (m) => bounded.send(m), close: () => socket2.close() };
76765
76861
  let ready = false;
76766
- let closed = false;
76862
+ let closed2 = false;
76767
76863
  const early = [];
76768
76864
  const attachReady = (async () => {
76769
76865
  const found = await getSessionAsync(id2);
@@ -76771,9 +76867,9 @@ async function terminal_default(app3, opts2) {
76771
76867
  socket2.close(4004, "not found");
76772
76868
  return;
76773
76869
  }
76774
- if (closed) return;
76870
+ if (closed2) return;
76775
76871
  await attachAsync(id2, client);
76776
- if (closed) {
76872
+ if (closed2) {
76777
76873
  detach(id2, client);
76778
76874
  return;
76779
76875
  }
@@ -76821,7 +76917,7 @@ async function terminal_default(app3, opts2) {
76821
76917
  const revalidate = watch ? setInterval(() => watch.refresh(), 6e4) : void 0;
76822
76918
  revalidate?.unref?.();
76823
76919
  socket2.on("close", () => {
76824
- closed = true;
76920
+ closed2 = true;
76825
76921
  bounded.dispose();
76826
76922
  if (revalidate) clearInterval(revalidate);
76827
76923
  watch?.dispose();
@@ -77179,9 +77275,9 @@ function sshExec(t, remoteCmd, opts2 = {}) {
77179
77275
  return new Promise((resolve24) => {
77180
77276
  const p3 = spawn4(sshBin(), args, { stdio: ["pipe", "pipe", "pipe"], env });
77181
77277
  let out4 = "";
77182
- const timer10 = setTimeout(() => p3.kill("SIGKILL"), opts2.timeoutMs ?? 6e4);
77278
+ const timer11 = setTimeout(() => p3.kill("SIGKILL"), opts2.timeoutMs ?? 6e4);
77183
77279
  const done = (r) => {
77184
- clearTimeout(timer10);
77280
+ clearTimeout(timer11);
77185
77281
  if (askpass) rmSync12(dirname15(askpass), { recursive: true, force: true });
77186
77282
  resolve24(r);
77187
77283
  };
@@ -78253,7 +78349,7 @@ function runCatalogCommand(command, agent, cwd, env, timeoutMs = TIMEOUT_MS) {
78253
78349
  const finish = (error, result) => {
78254
78350
  if (settled) return;
78255
78351
  settled = true;
78256
- clearTimeout(timer10);
78352
+ clearTimeout(timer11);
78257
78353
  child.stdin.end();
78258
78354
  if (child.exitCode === null && child.signalCode === null) {
78259
78355
  child.kill("SIGTERM");
@@ -78263,7 +78359,7 @@ function runCatalogCommand(command, agent, cwd, env, timeoutMs = TIMEOUT_MS) {
78263
78359
  if (error) reject2(error);
78264
78360
  else resolve24(result);
78265
78361
  };
78266
- const timer10 = setTimeout(() => finish(new Error("Pemeriksaan model melewati batas waktu")), timeoutMs);
78362
+ const timer11 = setTimeout(() => finish(new Error("Pemeriksaan model melewati batas waktu")), timeoutMs);
78267
78363
  child.on("error", () => finish(new Error("CLI model tidak dapat dijalankan")));
78268
78364
  child.stdin.on("error", () => finish(new Error("Kanal katalog CLI terputus")));
78269
78365
  child.stderr.on("data", () => {
@@ -78404,11 +78500,11 @@ var modelCatalogService = createModelCatalogService({
78404
78500
  });
78405
78501
  function startModelDiscovery() {
78406
78502
  void modelCatalogService.refresh();
78407
- const timer10 = setInterval(() => {
78503
+ const timer11 = setInterval(() => {
78408
78504
  void modelCatalogService.refresh();
78409
78505
  }, 5 * 6e4);
78410
- timer10.unref();
78411
- return () => clearInterval(timer10);
78506
+ timer11.unref();
78507
+ return () => clearInterval(timer11);
78412
78508
  }
78413
78509
 
78414
78510
  // src/routes/models.ts
@@ -78433,18 +78529,18 @@ function requestConfigRestart(delayMs = 50) {
78433
78529
  }
78434
78530
  function afterReplyFlushed(raw, fn, maxWaitMs = 5e3) {
78435
78531
  let done = false;
78436
- let timer10;
78532
+ let timer11;
78437
78533
  const run5 = () => {
78438
78534
  if (done) return;
78439
78535
  done = true;
78440
- if (timer10) clearTimeout(timer10);
78536
+ if (timer11) clearTimeout(timer11);
78441
78537
  raw.removeListener("finish", run5);
78442
78538
  raw.removeListener("close", run5);
78443
78539
  fn();
78444
78540
  };
78445
78541
  raw.once("finish", run5);
78446
78542
  raw.once("close", run5);
78447
- timer10 = setTimeout(run5, maxWaitMs);
78543
+ timer11 = setTimeout(run5, maxWaitMs);
78448
78544
  }
78449
78545
  function requestManualRestart(delayMs = 250) {
78450
78546
  setTimeout(() => process.exit(MANUAL_RESTART_EXIT), delayMs);
@@ -78511,6 +78607,364 @@ async function pendingCounts(now = Date.now(), prds = listAllPrds) {
78511
78607
  return { ...EMPTY_PENDING, triage: tickets2 + issues, backlog, prd, lead };
78512
78608
  }
78513
78609
 
78610
+ // src/services/overview-source.ts
78611
+ init_db();
78612
+
78613
+ // src/services/overview-metrics.ts
78614
+ init_src();
78615
+ var AGENT_KINDS = /* @__PURE__ */ new Set(["spec", "reverse", "prd", "scaffold", "breakdown", "telegram"]);
78616
+ var MIN_MS = 6e4;
78617
+ var HOUR_MS2 = 36e5;
78618
+ var startOfDay = (t) => {
78619
+ const d = new Date(t);
78620
+ return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
78621
+ };
78622
+ var addDays = (t, n2) => {
78623
+ const d = new Date(t);
78624
+ return new Date(d.getFullYear(), d.getMonth(), d.getDate() + n2, d.getHours(), d.getMinutes()).getTime();
78625
+ };
78626
+ var dayKey = (t) => {
78627
+ const d = new Date(t);
78628
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
78629
+ };
78630
+ var overlapMs = (a0, a1, b0, b1) => Math.max(0, Math.min(a1, b1) - Math.max(a0, b0));
78631
+ function median2(xs) {
78632
+ if (xs.length === 0) return null;
78633
+ const s2 = [...xs].sort((a, b) => a - b);
78634
+ const m = s2.length >> 1;
78635
+ return s2.length % 2 ? s2[m] : (s2[m - 1] + s2[m]) / 2;
78636
+ }
78637
+ var inRange = (d, t0, t1) => d !== null && d.getTime() >= t0 && d.getTime() < t1;
78638
+ var isFailed = (s2) => AGENT_KINDS.has(s2.kind) && s2.endedAt !== null && sessionOutcome({ endedAt: s2.endedAt.toISOString(), endedReason: s2.endedReason, exitCode: s2.exitCode }) === "failed";
78639
+ function dayStats(dayStart2, specs, sessions) {
78640
+ const dayEnd2 = addDays(dayStart2, 1);
78641
+ return {
78642
+ landed: specs.filter((s2) => inRange(s2.doneAt, dayStart2, dayEnd2)).length,
78643
+ failed: sessions.filter((s2) => isFailed(s2) && inRange(s2.endedAt, dayStart2, dayEnd2)).length
78644
+ };
78645
+ }
78646
+ function riverStats(now, specs) {
78647
+ const t0 = startOfDay(now);
78648
+ const t1 = addDays(t0, 1);
78649
+ const week0 = addDays(t0, -6);
78650
+ const done7 = specs.filter((s2) => inRange(s2.doneAt, week0, t1));
78651
+ return {
78652
+ created: specs.filter((s2) => inRange(s2.createdAt, t0, t1)).length,
78653
+ started: specs.filter((s2) => inRange(s2.startedAt, t0, t1)).length,
78654
+ landed: specs.filter((s2) => inRange(s2.doneAt, t0, t1)).length,
78655
+ leadTimeMedMs: median2(done7.map((s2) => s2.doneAt.getTime() - s2.createdAt.getTime())),
78656
+ cycleTimeMedMs: median2(done7.filter((s2) => s2.startedAt).map((s2) => s2.doneAt.getTime() - s2.startedAt.getTime()))
78657
+ };
78658
+ }
78659
+ var invTotal = (i) => (i.inputTokens ?? 0) + (i.outputTokens ?? 0) + (i.cachedTokens ?? 0);
78660
+ function tokenStats(now, invs, landedToday) {
78661
+ const t0 = startOfDay(now);
78662
+ const y0 = addDays(t0, -1);
78663
+ const today = invs.filter((i) => i.startedAt.getTime() >= t0);
78664
+ const input = today.reduce((n2, i) => n2 + (i.inputTokens ?? 0), 0);
78665
+ const output = today.reduce((n2, i) => n2 + (i.outputTokens ?? 0), 0);
78666
+ const cached3 = today.reduce((n2, i) => n2 + (i.cachedTokens ?? 0), 0);
78667
+ const total = input + output + cached3;
78668
+ const byAgent = /* @__PURE__ */ new Map();
78669
+ for (const i of today) byAgent.set(i.agentName, (byAgent.get(i.agentName) ?? 0) + invTotal(i));
78670
+ let topAgent = null;
78671
+ for (const [name2, n2] of byAgent) if (!topAgent || n2 > topAgent.total) topAgent = { name: name2, total: n2 };
78672
+ return {
78673
+ input,
78674
+ output,
78675
+ cached: cached3,
78676
+ total,
78677
+ cacheHitPct: input + cached3 > 0 ? Math.round(cached3 / (input + cached3) * 100) : null,
78678
+ perLanded: landedToday && landedToday > 0 ? Math.round(total / landedToday) : null,
78679
+ topAgent,
78680
+ yesterdayTotal: invs.filter((i) => i.startedAt.getTime() >= y0 && i.startedAt.getTime() < t0).reduce((n2, i) => n2 + invTotal(i), 0)
78681
+ };
78682
+ }
78683
+ function liveMs(sessions, t0, t1, now) {
78684
+ let ms = 0;
78685
+ for (const s2 of sessions) {
78686
+ if (!AGENT_KINDS.has(s2.kind)) continue;
78687
+ ms += overlapMs(s2.startedAt.getTime(), s2.endedAt ? s2.endedAt.getTime() : now, t0, t1);
78688
+ }
78689
+ return ms;
78690
+ }
78691
+ var spanMs = (spans, t0, t1, now) => spans.reduce((n2, w) => n2 + overlapMs(w.start, w.end ?? now, t0, t1), 0);
78692
+ function waitStats(now, waits, sessions) {
78693
+ const t0 = startOfDay(now);
78694
+ const t1 = addDays(t0, 1);
78695
+ const y0 = addDays(t0, -1);
78696
+ const todayMs = spanMs(waits, t0, t1, now);
78697
+ const live = sessions ? liveMs(sessions, t0, t1, now) : 0;
78698
+ return {
78699
+ todayMin: Math.round(todayMs / MIN_MS),
78700
+ yesterdayMin: Math.round(spanMs(waits, y0, t0, now) / MIN_MS),
78701
+ pctOfLive: sessions && live > 0 ? Math.min(100, Math.round(todayMs / live * 100)) : null
78702
+ };
78703
+ }
78704
+ function heatmapStats(now, sessions) {
78705
+ const t0 = startOfDay(now);
78706
+ const days2 = [];
78707
+ const minutes = [];
78708
+ for (let i = 6; i >= 0; i--) {
78709
+ const d0 = addDays(t0, -i);
78710
+ const base3 = new Date(d0);
78711
+ days2.push(dayKey(d0));
78712
+ const row = [];
78713
+ for (let h2 = 0; h2 < 24; h2++) {
78714
+ const h0 = new Date(base3.getFullYear(), base3.getMonth(), base3.getDate(), h2).getTime();
78715
+ const h1 = new Date(base3.getFullYear(), base3.getMonth(), base3.getDate(), h2 + 1).getTime();
78716
+ row.push(Math.round(liveMs(sessions, h0, h1, now) / MIN_MS));
78717
+ }
78718
+ minutes.push(row);
78719
+ }
78720
+ return { days: days2, minutes };
78721
+ }
78722
+ var RECORD_WINDOW_DAYS = 90;
78723
+ function recordStats(now, specs, sessions) {
78724
+ const t0 = startOfDay(now);
78725
+ const windowStart = addDays(t0, -(RECORD_WINDOW_DAYS - 1));
78726
+ const agent = sessions.filter((s2) => AGENT_KINDS.has(s2.kind));
78727
+ const failedDays = new Set(agent.filter(isFailed).map((s2) => dayKey(s2.endedAt.getTime())));
78728
+ let streakDays = 0;
78729
+ let bestStreak = 0;
78730
+ if (agent.length > 0) {
78731
+ const first = Math.max(windowStart, startOfDay(Math.min(...agent.map((s2) => s2.startedAt.getTime()))));
78732
+ let run5 = 0;
78733
+ for (let d = first; d <= t0; d = addDays(d, 1)) {
78734
+ run5 = failedDays.has(dayKey(d)) ? 0 : run5 + 1;
78735
+ bestStreak = Math.max(bestStreak, run5);
78736
+ }
78737
+ streakDays = run5;
78738
+ }
78739
+ const landedByDay = /* @__PURE__ */ new Map();
78740
+ for (const s2 of specs) {
78741
+ if (!s2.doneAt || s2.doneAt.getTime() < windowStart) continue;
78742
+ const k = dayKey(s2.doneAt.getTime());
78743
+ landedByDay.set(k, (landedByDay.get(k) ?? 0) + 1);
78744
+ }
78745
+ let bestLandedDay = null;
78746
+ for (const [date, count] of [...landedByDay].sort(([a], [b]) => a.localeCompare(b)))
78747
+ if (!bestLandedDay || count > bestLandedDay.count) bestLandedDay = { date, count };
78748
+ const todayKey = dayKey(t0);
78749
+ const todayCount = landedByDay.get(todayKey) ?? 0;
78750
+ const prevMax = Math.max(0, ...[...landedByDay].filter(([k]) => k !== todayKey).map(([, n2]) => n2));
78751
+ return { streakDays, bestStreak, bestLandedDay, landedRecordToday: prevMax > 0 && todayCount > prevMax };
78752
+ }
78753
+ var BENSIN_LOOKBACK_MS = 60 * MIN_MS;
78754
+ var BENSIN_MIN_SAMPLES = 4;
78755
+ var BENSIN_MIN_SPAN_MS = 15 * MIN_MS;
78756
+ function slopeOf(pts) {
78757
+ const n2 = pts.length;
78758
+ const mx = pts.reduce((s2, p3) => s2 + p3.at, 0) / n2;
78759
+ const my = pts.reduce((s2, p3) => s2 + p3.pct, 0) / n2;
78760
+ let num = 0;
78761
+ let den = 0;
78762
+ for (const p3 of pts) {
78763
+ num += (p3.at - mx) * (p3.pct - my);
78764
+ den += (p3.at - mx) ** 2;
78765
+ }
78766
+ return den === 0 ? 0 : num / den;
78767
+ }
78768
+ function bensinStats(now, series2) {
78769
+ const out4 = [];
78770
+ for (const s2 of series2) {
78771
+ const last2 = s2.samples.at(-1);
78772
+ if (!last2) continue;
78773
+ const recent = s2.samples.filter((p3) => p3.at >= now - BENSIN_LOOKBACK_MS);
78774
+ const base3 = {
78775
+ source: s2.source,
78776
+ key: s2.key,
78777
+ label: s2.label,
78778
+ usedPct: last2.pct,
78779
+ resetsAt: s2.resetsAt,
78780
+ samples: recent.map((p3) => ({ at: new Date(p3.at).toISOString(), pct: p3.pct }))
78781
+ };
78782
+ if (recent.length < BENSIN_MIN_SAMPLES || last2.at - recent[0].at < BENSIN_MIN_SPAN_MS) {
78783
+ out4.push({ ...base3, exhaustAt: null, verdict: "kurang-data" });
78784
+ continue;
78785
+ }
78786
+ const slope = slopeOf(recent);
78787
+ if (slope <= 0) {
78788
+ out4.push({ ...base3, exhaustAt: null, verdict: "stabil" });
78789
+ continue;
78790
+ }
78791
+ const exhaust = Math.round(last2.at + (100 - last2.pct) / slope);
78792
+ const reset = s2.resetsAt ? Date.parse(s2.resetsAt) : Number.NaN;
78793
+ if (Number.isFinite(reset) && exhaust >= reset) out4.push({ ...base3, exhaustAt: null, verdict: "aman" });
78794
+ else out4.push({ ...base3, exhaustAt: new Date(exhaust).toISOString(), verdict: "habis" });
78795
+ }
78796
+ return out4;
78797
+ }
78798
+ function fmtMinutes(min) {
78799
+ if (min < 60) return `${min} menit`;
78800
+ const h2 = Math.floor(min / 60);
78801
+ const m = min % 60;
78802
+ return m ? `${h2} jam ${m} menit` : `${h2} jam`;
78803
+ }
78804
+ function fmtClock(t) {
78805
+ const d = new Date(t);
78806
+ return `${String(d.getHours()).padStart(2, "0")}.${String(d.getMinutes()).padStart(2, "0")}`;
78807
+ }
78808
+ var WAIT_HEADLINE_MIN = 30;
78809
+ function headline(o) {
78810
+ const facts = [];
78811
+ if (o.today && o.today.failed > 0) facts.push({ text: `${o.today.failed} sesi tumbang`, tone: "bad" });
78812
+ if (o.wait && o.wait.todayMin >= WAIT_HEADLINE_MIN)
78813
+ facts.push({ text: `agen menunggumu ${fmtMinutes(o.wait.todayMin)}`, tone: "bad" });
78814
+ const record2 = !!o.records?.landedRecordToday && !!o.today && o.today.landed > 0;
78815
+ if (record2) facts.push({ text: `rekor baru: ${o.today.landed} spec mendarat`, tone: "good" });
78816
+ const empty = o.bensin?.find((b) => b.verdict === "habis" && b.exhaustAt);
78817
+ if (empty) facts.push({ text: `limit ${empty.label} habis \xB1${fmtClock(Date.parse(empty.exhaustAt))}`, tone: "bad" });
78818
+ if (!record2 && o.today && o.today.landed > 0) facts.push({ text: `${o.today.landed} spec mendarat`, tone: "good" });
78819
+ const top = facts.slice(0, 3);
78820
+ if (top.length === 0) return { text: "Hari yang tenang.", parts: [{ text: "Hari yang tenang." }] };
78821
+ top[0] = { ...top[0], text: top[0].text.charAt(0).toUpperCase() + top[0].text.slice(1) };
78822
+ const parts = [];
78823
+ top.forEach((f, i) => {
78824
+ if (i > 0) parts.push({ text: i === top.length - 1 ? top.length === 2 ? " dan " : ", dan " : ", " });
78825
+ parts.push(f);
78826
+ });
78827
+ parts.push({ text: "." });
78828
+ return { text: parts.map((p3) => p3.text).join(""), parts };
78829
+ }
78830
+ function safe(label2, f) {
78831
+ try {
78832
+ return f();
78833
+ } catch (err) {
78834
+ console.warn(`overview: panel ${label2} gagal dihitung:`, err);
78835
+ return null;
78836
+ }
78837
+ }
78838
+ function computeOverview(now, src) {
78839
+ const { specs, sessions, invocations } = src;
78840
+ const t0 = startOfDay(now);
78841
+ const today = specs && sessions ? safe("today", () => dayStats(t0, specs, sessions)) : null;
78842
+ const yesterday = specs && sessions ? safe("yesterday", () => dayStats(addDays(t0, -1), specs, sessions)) : null;
78843
+ const wait = safe("wait", () => waitStats(now, src.waits, sessions));
78844
+ const bensin = safe("bensin", () => bensinStats(now, src.limits));
78845
+ const records = specs && sessions ? safe("records", () => recordStats(now, specs, sessions)) : null;
78846
+ return {
78847
+ generatedAt: new Date(now).toISOString(),
78848
+ recordingSince: new Date(src.recordingSince).toISOString(),
78849
+ today,
78850
+ yesterday,
78851
+ wait,
78852
+ bensin,
78853
+ records,
78854
+ river: specs ? safe("river", () => riverStats(now, specs)) : null,
78855
+ tokens: invocations ? safe("tokens", () => tokenStats(now, invocations, today?.landed ?? null)) : null,
78856
+ heatmap: sessions ? safe("heatmap", () => heatmapStats(now, sessions)) : null,
78857
+ headline: safe("headline", () => headline({ today, wait, records, bensin })) ?? { text: "Hari yang tenang.", parts: [{ text: "Hari yang tenang." }] }
78858
+ };
78859
+ }
78860
+
78861
+ // src/services/overview-recorder.ts
78862
+ init_pty();
78863
+ var MAX_EPISODES = 2e3;
78864
+ var KEEP_MS = 2 * HOUR_MS2;
78865
+ var MIN_GAP_MS = 25e3;
78866
+ var TICK_MS2 = 1e4;
78867
+ var LIMIT_EVERY_TICKS = 3;
78868
+ var since = Date.now();
78869
+ var open5 = /* @__PURE__ */ new Map();
78870
+ var closed = [];
78871
+ var series = /* @__PURE__ */ new Map();
78872
+ var timer4;
78873
+ var ticks = 0;
78874
+ function observeSessions(list2, now = Date.now()) {
78875
+ const waiting2 = /* @__PURE__ */ new Set();
78876
+ for (const s2 of list2) {
78877
+ if (s2.exited || !s2.decision || s2.deciding) continue;
78878
+ waiting2.add(s2.id);
78879
+ if (open5.has(s2.id)) continue;
78880
+ const at = s2.decisionAt ? Date.parse(s2.decisionAt) : Number.NaN;
78881
+ open5.set(s2.id, Number.isFinite(at) ? Math.min(at, now) : now);
78882
+ }
78883
+ for (const [id2, start] of open5) {
78884
+ if (waiting2.has(id2)) continue;
78885
+ open5.delete(id2);
78886
+ closed.push({ start, end: now });
78887
+ }
78888
+ if (closed.length > MAX_EPISODES) closed = closed.slice(-MAX_EPISODES);
78889
+ }
78890
+ function observeLimits(source, windows, now = Date.now()) {
78891
+ for (const w of windows) {
78892
+ const k = `${source}:${w.key}`;
78893
+ let s2 = series.get(k);
78894
+ if (!s2) {
78895
+ s2 = { source, key: w.key, label: w.label, resetsAt: w.resetsAt, samples: [] };
78896
+ series.set(k, s2);
78897
+ }
78898
+ s2.label = w.label;
78899
+ s2.resetsAt = w.resetsAt;
78900
+ const last2 = s2.samples.at(-1);
78901
+ if (last2 && w.usedPct < last2.pct) s2.samples = [];
78902
+ else if (last2 && now - last2.at < MIN_GAP_MS) continue;
78903
+ s2.samples.push({ at: now, pct: w.usedPct });
78904
+ s2.samples = s2.samples.filter((p3) => p3.at >= now - KEEP_MS);
78905
+ }
78906
+ }
78907
+ var waitSpans = () => [...closed, ...[...open5.values()].map((start) => ({ start, end: null }))];
78908
+ var limitSeries = () => [...series.values()].map((s2) => ({ ...s2, samples: [...s2.samples] }));
78909
+ var recordingSince = () => since;
78910
+ async function tick() {
78911
+ const list2 = await listSessionsAsync();
78912
+ observeSessions(list2.map((s2) => ({ ...s2, deciding: isDeciding(s2.id) })));
78913
+ if (ticks++ % LIMIT_EVERY_TICKS !== 0) return;
78914
+ const [claude, codex2] = await Promise.all([getLimits(), getCodexLimits()]);
78915
+ if (claude.status !== "unavailable") observeLimits("claude", claude.windows);
78916
+ if (codex2.status !== "unavailable") observeLimits("codex", codex2.windows);
78917
+ }
78918
+ function startOverviewRecorder() {
78919
+ if (timer4) return;
78920
+ void tick().catch((e) => console.warn("perekam overview:", e));
78921
+ timer4 = setInterval(() => {
78922
+ void tick().catch(() => {
78923
+ });
78924
+ }, TICK_MS2);
78925
+ timer4.unref();
78926
+ }
78927
+
78928
+ // src/services/overview-source.ts
78929
+ async function settle2(label2, p3) {
78930
+ try {
78931
+ return await p3;
78932
+ } catch (err) {
78933
+ console.warn(`overview: sumber ${label2} gagal:`, err);
78934
+ return null;
78935
+ }
78936
+ }
78937
+ var RECORD_WINDOW_DAYS2 = 90;
78938
+ async function getOverview(now = Date.now()) {
78939
+ const since2 = new Date(addDays(startOfDay(now), -(RECORD_WINDOW_DAYS2 - 1)));
78940
+ const sinceYesterday = new Date(addDays(startOfDay(now), -1));
78941
+ const [specs, sessions, invocations] = await Promise.all([
78942
+ settle2("spec", prisma.spec.findMany({
78943
+ where: { OR: [{ createdAt: { gte: since2 } }, { startedAt: { gte: since2 } }, { doneAt: { gte: since2 } }] },
78944
+ select: { createdAt: true, startedAt: true, doneAt: true }
78945
+ })),
78946
+ settle2("sessionHistory", prisma.sessionHistory.findMany({
78947
+ where: {
78948
+ kind: { in: [...AGENT_KINDS] },
78949
+ OR: [{ startedAt: { gte: since2 } }, { endedAt: null }, { endedAt: { gte: since2 } }]
78950
+ },
78951
+ select: { kind: true, startedAt: true, endedAt: true, exitCode: true, endedReason: true }
78952
+ })),
78953
+ settle2("agentInvocation", prisma.agentInvocation.findMany({
78954
+ where: { startedAt: { gte: sinceYesterday } },
78955
+ select: { agentName: true, startedAt: true, inputTokens: true, outputTokens: true, cachedTokens: true }
78956
+ }))
78957
+ ]);
78958
+ return computeOverview(now, {
78959
+ specs,
78960
+ sessions,
78961
+ invocations,
78962
+ waits: waitSpans(),
78963
+ limits: limitSeries(),
78964
+ recordingSince: recordingSince()
78965
+ });
78966
+ }
78967
+
78514
78968
  // src/services/events.ts
78515
78969
  init_db();
78516
78970
  init_config2();
@@ -79293,11 +79747,11 @@ var HOUSEKEEPING_MS = 6e4;
79293
79747
  var busyPulse = false;
79294
79748
  var lastPulseAt = 0;
79295
79749
  var pulseEndedAt = 0;
79296
- var timer4;
79750
+ var timer5;
79297
79751
  function lastPulse() {
79298
79752
  return lastPulseAt;
79299
79753
  }
79300
- async function tick(now, deps = {}) {
79754
+ async function tick2(now, deps = {}) {
79301
79755
  let cfg;
79302
79756
  try {
79303
79757
  cfg = await (deps.cfg ?? getLead)();
@@ -79348,10 +79802,10 @@ async function tick(now, deps = {}) {
79348
79802
  await Promise.all(jobs);
79349
79803
  }
79350
79804
  function startLead(deps = {}) {
79351
- if (timer4) return;
79352
- timer4 = setInterval(() => void tick(Date.now(), deps), HOUSEKEEPING_MS);
79353
- timer4.unref();
79354
- void tick(Date.now(), deps);
79805
+ if (timer5) return;
79806
+ timer5 = setInterval(() => void tick2(Date.now(), deps), HOUSEKEEPING_MS);
79807
+ timer5.unref();
79808
+ void tick2(Date.now(), deps);
79355
79809
  }
79356
79810
 
79357
79811
  // src/services/lead/views.ts
@@ -79362,7 +79816,7 @@ async function buildLeadStatus() {
79362
79816
  select: { id: true, name: true },
79363
79817
  orderBy: { id: "asc" }
79364
79818
  });
79365
- const since = new Date(Date.now() - 24 * 60 * 60 * 1e3);
79819
+ const since2 = new Date(Date.now() - 24 * 60 * 60 * 1e3);
79366
79820
  let live = [];
79367
79821
  try {
79368
79822
  live = (await listSessionsAsync()).filter((s2) => !s2.exited);
@@ -79378,7 +79832,7 @@ async function buildLeadStatus() {
79378
79832
  name: p3.name,
79379
79833
  optIn: true,
79380
79834
  paused: !leadActive(cfg, p3.id),
79381
- decisions24h: await prisma.leadDecision.count({ where: { projectId: p3.id, createdAt: { gte: since } } }),
79835
+ decisions24h: await prisma.leadDecision.count({ where: { projectId: p3.id, createdAt: { gte: since2 } } }),
79382
79836
  openSessions: live.filter((s2) => s2.projectId === p3.id).length
79383
79837
  })));
79384
79838
  const last2 = lastPulse();
@@ -79550,7 +80004,16 @@ var GROUPS = [
79550
80004
  // bukan empat grup: keempatnya berubah dari peristiwa yang sama (item diajukan/diputuskan) dan
79551
80005
  // dibaca satu komponen yang sama. `cookieOnly` TIDAK dipasang — muatannya jumlah agregat tanpa
79552
80006
  // id/judul, dan agent token sudah boleh membaca daftar sumbernya lewat capability masing-masing.
79553
- { everyTicks: 5, last: "", build: async () => ({ t: "pending", counts: await pendingCounts() }) }
80007
+ { everyTicks: 5, last: "", build: async () => ({ t: "pending", counts: await pendingCounts() }) },
80008
+ // ADR-0220 · grup GLOBAL ke-12 · agregat Overview "Koran Pagi". 30 dtk: angkanya harian, bukan
80009
+ // detik-an. `generatedAt` berubah tiap build, jadi kunci dedup mengosongkannya; frame lahir hanya
80010
+ // saat isi agregat benar-benar berubah. Tidak `cookieOnly`: angka agregat tanpa id/judul.
80011
+ {
80012
+ everyTicks: 30,
80013
+ last: "",
80014
+ build: async () => ({ t: "overview", overview: await getOverview() }),
80015
+ sig: (m) => JSON.stringify({ ...m.overview, generatedAt: null })
80016
+ }
79554
80017
  ];
79555
80018
  function broadcast2(msg, s2, cookieOnly = false) {
79556
80019
  for (const c of clients) {
@@ -79637,9 +80100,9 @@ function subscribeClient(c, subs) {
79637
80100
  if (e.subscribers.size === 0) entries2.delete(key);
79638
80101
  }
79639
80102
  }
79640
- var tick2 = 0;
80103
+ var tick3 = 0;
79641
80104
  var busy = false;
79642
- var timer5;
80105
+ var timer6;
79643
80106
  async function runGroup(g) {
79644
80107
  const t0 = profStart();
79645
80108
  let msg;
@@ -79679,9 +80142,9 @@ async function runGroup(g) {
79679
80142
  async function __tick() {
79680
80143
  if (busy) return;
79681
80144
  busy = true;
79682
- tick2++;
80145
+ tick3++;
79683
80146
  try {
79684
- const results = await Promise.all(GROUPS.filter((g) => tick2 % g.everyTicks === 0).map(runGroup));
80147
+ const results = await Promise.all(GROUPS.filter((g) => tick3 % g.everyTicks === 0).map(runGroup));
79685
80148
  for (const r of results) {
79686
80149
  if (!r) continue;
79687
80150
  r.g.last = r.json;
@@ -79699,19 +80162,19 @@ async function __tick() {
79699
80162
  }
79700
80163
  }
79701
80164
  function startLoop() {
79702
- if (timer5) return;
80165
+ if (timer6) return;
79703
80166
  startLagMonitor();
79704
- timer5 = setInterval(() => {
80167
+ timer6 = setInterval(() => {
79705
80168
  void __tick();
79706
80169
  }, tickMs());
79707
- timer5.unref();
80170
+ timer6.unref();
79708
80171
  }
79709
80172
  function stopLoop() {
79710
- if (timer5) {
79711
- clearInterval(timer5);
79712
- timer5 = void 0;
80173
+ if (timer6) {
80174
+ clearInterval(timer6);
80175
+ timer6 = void 0;
79713
80176
  }
79714
- tick2 = 0;
80177
+ tick3 = 0;
79715
80178
  for (const g of GROUPS) {
79716
80179
  g.last = "";
79717
80180
  g.lastSig = void 0;
@@ -80356,6 +80819,11 @@ async function presence_default(app3) {
80356
80819
  app3.get("/presence", async () => presenceView());
80357
80820
  }
80358
80821
 
80822
+ // src/routes/overview.ts
80823
+ async function overview_default(app3) {
80824
+ app3.get("/overview", async () => getOverview());
80825
+ }
80826
+
80359
80827
  // src/routes/session-results.ts
80360
80828
  init_db();
80361
80829
  var view6 = (r) => ({
@@ -80666,7 +81134,7 @@ async function intakeTicket(input) {
80666
81134
  let ticketBytes = 0;
80667
81135
  for (const f of input.files) {
80668
81136
  try {
80669
- const safe2 = await processUpload({
81137
+ const safe3 = await processUpload({
80670
81138
  buffer: f.buf,
80671
81139
  clientName: f.name,
80672
81140
  clientMime: f.mime,
@@ -80679,17 +81147,17 @@ async function intakeTicket(input) {
80679
81147
  data: {
80680
81148
  ticketId: ticket.id,
80681
81149
  projectId: input.projectId,
80682
- filename: safe2.filename,
80683
- mimeType: safe2.mimeType,
80684
- size: safe2.size,
80685
- storageKey: safe2.storageKey
81150
+ filename: safe3.filename,
81151
+ mimeType: safe3.mimeType,
81152
+ size: safe3.size,
81153
+ storageKey: safe3.storageKey
80686
81154
  }
80687
81155
  });
80688
81156
  } catch (error) {
80689
- await deleteUpload(safe2.storageKey);
81157
+ await deleteUpload(safe3.storageKey);
80690
81158
  throw error;
80691
81159
  }
80692
- ticketBytes += safe2.size;
81160
+ ticketBytes += safe3.size;
80693
81161
  await notifySynced("ticketAttachment", att.id);
80694
81162
  } catch (error) {
80695
81163
  if (!(error instanceof UploadError)) throw error;
@@ -82709,7 +83177,7 @@ var QaImportError = class extends Error {
82709
83177
  this.status = status2;
82710
83178
  }
82711
83179
  };
82712
- var safe = (s2) => s2.replace(/[^A-Za-z0-9._-]/g, "_");
83180
+ var safe2 = (s2) => s2.replace(/[^A-Za-z0-9._-]/g, "_");
82713
83181
  var MIME_BY_EXT = {
82714
83182
  png: "image/png",
82715
83183
  jpg: "image/jpeg",
@@ -82737,7 +83205,7 @@ async function exportReport(projectId, reportId) {
82737
83205
  const owner = codeOf(a.ownerType, a.ownerId);
82738
83206
  const n2 = (seq.get(owner) ?? 0) + 1;
82739
83207
  seq.set(owner, n2);
82740
- paths2[a.id] = `attachments/${owner}-${n2}-${safe(a.filename)}`;
83208
+ paths2[a.id] = `attachments/${owner}-${n2}-${safe2(a.filename)}`;
82741
83209
  files2.push({ name: paths2[a.id], data });
82742
83210
  present.push(a);
82743
83211
  }
@@ -84163,7 +84631,7 @@ async function testTelegramConnection(deps = {}) {
84163
84631
  }
84164
84632
  const timeoutMs = deps.timeoutMs ?? 1e4;
84165
84633
  const controller = new AbortController();
84166
- const timer10 = setTimeout(() => controller.abort(), timeoutMs);
84634
+ const timer11 = setTimeout(() => controller.abort(), timeoutMs);
84167
84635
  const send = deps.transport ?? ((url2, init) => fetch(url2, init));
84168
84636
  const transport = (url2, init) => send(url2, { ...init, signal: controller.signal });
84169
84637
  const client = new TelegramApiClient(botToken, transport);
@@ -84177,7 +84645,7 @@ async function testTelegramConnection(deps = {}) {
84177
84645
  const message = controller.signal.aborted ? `Timeout ${timeoutMs} ms \u2014 Telegram tidak menjawab.` : raw;
84178
84646
  return { ok: false, inbound, error: sanitizeTelegramOutput(message, [botToken]).slice(0, 500) };
84179
84647
  } finally {
84180
- clearTimeout(timer10);
84648
+ clearTimeout(timer11);
84181
84649
  }
84182
84650
  }
84183
84651
  function resolveTestChatId() {
@@ -84411,7 +84879,7 @@ async function sendOnce(o, deps = {}) {
84411
84879
  nowSec: o.nowSec ?? Math.floor(Date.now() / 1e3)
84412
84880
  });
84413
84881
  const ac = new AbortController();
84414
- const timer10 = setTimeout(() => ac.abort(), WEBHOOK_TIMEOUT_MS);
84882
+ const timer11 = setTimeout(() => ac.abort(), WEBHOOK_TIMEOUT_MS);
84415
84883
  try {
84416
84884
  const res = deps.fetcher ? await deps.fetcher(parsed.url.toString(), {
84417
84885
  method: "POST",
@@ -84445,7 +84913,7 @@ async function sendOnce(o, deps = {}) {
84445
84913
  const msg = e.name === "AbortError" ? `timeout ${WEBHOOK_TIMEOUT_MS} ms` : e.message;
84446
84914
  return fail4(msg);
84447
84915
  } finally {
84448
- clearTimeout(timer10);
84916
+ clearTimeout(timer11);
84449
84917
  }
84450
84918
  }
84451
84919
 
@@ -84840,10 +85308,10 @@ function sanitizeClientText(text2) {
84840
85308
  return text2.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, "").slice(0, MAX_PESAN);
84841
85309
  }
84842
85310
  function wrapClientMessage(text2, nonce) {
84843
- const open5 = `<pesan-klien-${nonce}>`;
85311
+ const open6 = `<pesan-klien-${nonce}>`;
84844
85312
  const close = `</pesan-klien-${nonce}>`;
84845
85313
  const jinak = sanitizeClientText(text2).replaceAll("</pesan-klien", "<\u200B/pesan-klien").replaceAll("<pesan-klien", "<\u200Bpesan-klien");
84846
- return `${open5}
85314
+ return `${open6}
84847
85315
  ${jinak}
84848
85316
  ${close}`;
84849
85317
  }
@@ -86731,6 +87199,7 @@ function buildApp({ requireAuth = true, agentDocFile, env = process.env } = {})
86731
87199
  await api.register(bindings_default);
86732
87200
  await api.register(sync_default);
86733
87201
  await api.register(presence_default);
87202
+ await api.register(overview_default);
86734
87203
  await api.register(session_results_default);
86735
87204
  await api.register(session_history_default);
86736
87205
  await api.register(session_events_default);
@@ -86776,17 +87245,17 @@ function createShutdown(o) {
86776
87245
  let running;
86777
87246
  return (sig) => running ??= (async () => {
86778
87247
  log(`${sig} \u2014 menutup`);
86779
- const timer10 = setTimeout(() => {
87248
+ const timer11 = setTimeout(() => {
86780
87249
  log(`penutupan melewati ${o.timeoutMs ?? DEFAULT_TIMEOUT_MS} ms \u2014 keluar paksa`);
86781
87250
  o.exit(1);
86782
87251
  }, o.timeoutMs ?? DEFAULT_TIMEOUT_MS);
86783
- timer10.unref?.();
87252
+ timer11.unref?.();
86784
87253
  try {
86785
87254
  await o.close();
86786
87255
  } catch (e) {
86787
87256
  log(`penutupan gagal: ${e.message}`);
86788
87257
  }
86789
- clearTimeout(timer10);
87258
+ clearTimeout(timer11);
86790
87259
  o.exit(0);
86791
87260
  })();
86792
87261
  }
@@ -87051,7 +87520,7 @@ var prodEnd = {
87051
87520
  reconcile: () => reconcile(reconcileProdDeps),
87052
87521
  scanDecisions: () => scanDecisions()
87053
87522
  };
87054
- async function tick3(now, deps, end2 = prodEnd) {
87523
+ async function tick4(now, deps, end2 = prodEnd) {
87055
87524
  const cfg = await getScheduler();
87056
87525
  if (!cfg.enabled) return;
87057
87526
  for (const src of listSources()) {
@@ -87116,13 +87585,13 @@ var prodDeps = {
87116
87585
  launchCron: async (cron) => (await startCronSession(cron)).id
87117
87586
  })
87118
87587
  };
87119
- var TICK_MS2 = 1e4;
87120
- var timer6;
87588
+ var TICK_MS3 = 1e4;
87589
+ var timer7;
87121
87590
  function startScheduler(deps = prodDeps) {
87122
- if (timer6) return;
87123
- timer6 = setInterval(() => void tick3(Date.now(), deps), TICK_MS2);
87124
- timer6.unref();
87125
- void tick3(Date.now(), deps);
87591
+ if (timer7) return;
87592
+ timer7 = setInterval(() => void tick4(Date.now(), deps), TICK_MS3);
87593
+ timer7.unref();
87594
+ void tick4(Date.now(), deps);
87126
87595
  }
87127
87596
 
87128
87597
  // src/services/scheduler/sources/backlog.ts
@@ -87294,7 +87763,7 @@ async function installWebhooks() {
87294
87763
  // src/services/webhooks/engine.ts
87295
87764
  init_src();
87296
87765
  init_db();
87297
- var TICK_MS3 = 2e3;
87766
+ var TICK_MS4 = 2e3;
87298
87767
  var MAX_IN_FLIGHT = 4;
87299
87768
  var PRUNE_EVERY_TICKS = 30;
87300
87769
  function backoffAt(from, attempt) {
@@ -87437,15 +87906,15 @@ async function pruneHistory() {
87437
87906
  }
87438
87907
  return removed;
87439
87908
  }
87440
- var timer7;
87441
- var ticks = 0;
87909
+ var timer8;
87910
+ var ticks2 = 0;
87442
87911
  var busy2 = false;
87443
- async function tick4() {
87912
+ async function tick5() {
87444
87913
  if (busy2) return;
87445
87914
  busy2 = true;
87446
87915
  try {
87447
87916
  await deliverDue(/* @__PURE__ */ new Date());
87448
- if (++ticks % PRUNE_EVERY_TICKS === 0) await pruneHistory();
87917
+ if (++ticks2 % PRUNE_EVERY_TICKS === 0) await pruneHistory();
87449
87918
  } catch (e) {
87450
87919
  console.error("webhook engine:", e);
87451
87920
  } finally {
@@ -87453,9 +87922,9 @@ async function tick4() {
87453
87922
  }
87454
87923
  }
87455
87924
  function startWebhookEngine() {
87456
- if (timer7) return;
87457
- timer7 = setInterval(() => void tick4(), TICK_MS3);
87458
- timer7.unref();
87925
+ if (timer8) return;
87926
+ timer8 = setInterval(() => void tick5(), TICK_MS4);
87927
+ timer8.unref();
87459
87928
  void resetStuckDeliveries().then((n2) => {
87460
87929
  if (n2) console.log(`webhook: ${n2} pengiriman tertinggal dikembalikan ke antrean`);
87461
87930
  }).catch((e) => console.error("webhook reset:", e));
@@ -87470,7 +87939,7 @@ init_local_binding();
87470
87939
  init_notifications2();
87471
87940
  var AUTO_MERGE_WINDOW_MS = 24 * 60 * 60 * 1e3;
87472
87941
  var AUTO_MERGE_GRACE_MS = 15 * 60 * 1e3;
87473
- var TICK_MS4 = 6e4;
87942
+ var TICK_MS5 = 6e4;
87474
87943
  var exec11 = promisify17(execFile20);
87475
87944
  var GIT8 = { timeout: 3e4, maxBuffer: 1 << 24, encoding: "utf8" };
87476
87945
  var gitOut = async (cwd, args) => {
@@ -87493,9 +87962,9 @@ var prodAutoMergeDeps = {
87493
87962
  deleteBranch: deleteMergedBranch
87494
87963
  };
87495
87964
  async function candidates(now) {
87496
- const since = new Date(now.getTime() - AUTO_MERGE_WINDOW_MS);
87965
+ const since2 = new Date(now.getTime() - AUTO_MERGE_WINDOW_MS);
87497
87966
  const done = await prisma.notification.findMany({
87498
- where: { type: "done", createdAt: { gte: since }, specId: { not: null } },
87967
+ where: { type: "done", createdAt: { gte: since2 }, specId: { not: null } },
87499
87968
  select: { specId: true, projectId: true, createdAt: true }
87500
87969
  });
87501
87970
  if (!done.length) return [];
@@ -87559,9 +88028,9 @@ async function settleOne(c, deps, now) {
87559
88028
  }
87560
88029
  return report(`Auto-merge ${spec.id} \u2192 ${target3} GAGAL: ${res.error}`);
87561
88030
  }
87562
- var timer8;
88031
+ var timer9;
87563
88032
  var busy3 = false;
87564
- async function tick5() {
88033
+ async function tick6() {
87565
88034
  if (busy3) return;
87566
88035
  busy3 = true;
87567
88036
  try {
@@ -87573,9 +88042,9 @@ async function tick5() {
87573
88042
  }
87574
88043
  }
87575
88044
  function startAutoMerge() {
87576
- if (timer8) return;
87577
- timer8 = setInterval(() => void tick5(), TICK_MS4);
87578
- timer8.unref();
88045
+ if (timer9) return;
88046
+ timer9 = setInterval(() => void tick6(), TICK_MS5);
88047
+ timer9.unref();
87579
88048
  }
87580
88049
 
87581
88050
  // src/server.ts
@@ -87874,9 +88343,9 @@ async function pruneSyncFeed(now, dryRun = false) {
87874
88343
  }
87875
88344
  return pruned;
87876
88345
  }
87877
- var timer9;
88346
+ var timer10;
87878
88347
  function startRetentionSweep() {
87879
- if (timer9) return;
88348
+ if (timer10) return;
87880
88349
  const sweep = () => {
87881
88350
  const holds = new Set((process.env.HANOMAN_RETENTION_HOLDS ?? "").split(",").map((v) => v.trim()).filter(Boolean));
87882
88351
  void runRetention({ holds }).then((report) => {
@@ -87886,8 +88355,8 @@ function startRetentionSweep() {
87886
88355
  }).catch((error) => console.error("retention sweep:", error));
87887
88356
  };
87888
88357
  sweep();
87889
- timer9 = setInterval(sweep, 24 * 60 * 6e4);
87890
- timer9.unref?.();
88358
+ timer10 = setInterval(sweep, 24 * 60 * 6e4);
88359
+ timer10.unref?.();
87891
88360
  }
87892
88361
 
87893
88362
  // src/server.ts
@@ -87987,6 +88456,7 @@ bootstrapReady.then(async () => {
87987
88456
  startAutoMerge();
87988
88457
  startWorktreeReaper();
87989
88458
  startRetentionSweep();
88459
+ startOverviewRecorder();
87990
88460
  }).catch((err) => {
87991
88461
  console.error("listen gagal:", err);
87992
88462
  process.exit(1);