hanoman 0.9.27 → 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 +667 -202
  4. package/docs/agent-integration.md +1 -0
  5. package/package.json +1 -1
  6. package/web/assets/{DalangHanomanScreen-wJskhX1_.js → DalangHanomanScreen-C3UMPHFg.js} +1 -1
  7. package/web/assets/{DocPreviewModal-ClHbRVmQ.js → DocPreviewModal-Bp5stb4M.js} +1 -1
  8. package/web/assets/{DocsWorkspace-Db_g219X.js → DocsWorkspace-C9yFxK4D.js} +1 -1
  9. package/web/assets/{IdeScreen-CfX1HEtj.js → IdeScreen-CRFRAP6E.js} +1 -1
  10. package/web/assets/{LeadScreen-BIpisTdG.js → LeadScreen-DfVUawoK.js} +1 -1
  11. package/web/assets/{ReviewScreen-DMGTLCki.js → ReviewScreen-CMHOlmgX.js} +1 -1
  12. package/web/assets/{SchedulerScreen-CSaFiXc9.js → SchedulerScreen-CK5QvX5c.js} +1 -1
  13. package/web/assets/{SettingsScreen-C4chALvS.js → SettingsScreen-D7P5JgjU.js} +1 -1
  14. package/web/assets/{TeamScreen-CS9osC8I.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-Eh8BoPTm.js → TriageScreen-B8ZCexaX.js} +1 -1
  18. package/web/assets/{VpsScreen-CweCi934.js → VpsScreen-BCrLfZh7.js} +1 -1
  19. package/web/assets/{index-Av-RfHXX.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-Bfx0Oehi.js → live-DuBdI0MZ.js} +1 -1
  22. package/web/assets/{sched-lead-shared-DJn2D3VE.js → sched-lead-shared-QqbtjoVv.js} +1 -1
  23. package/web/assets/{settings-rebase-Bbz53ceL.js → settings-rebase-Cbh-JGJ-.js} +1 -1
  24. package/web/index.html +2 -2
  25. package/web/assets/TerminalScreen-C0fHHsUa.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();
@@ -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;
@@ -54173,9 +54264,9 @@ function recordBytes(rec) {
54173
54264
  return Buffer.byteLength(JSON.stringify(rec));
54174
54265
  }
54175
54266
  async function pull(sinceCursor, limit = 500, maxBytes = PULL_MAX_BYTES, opts2 = {}) {
54176
- const since = Number(sinceCursor || "0");
54267
+ const since2 = Number(sinceCursor || "0");
54177
54268
  const rows = await prisma.syncLog.findMany({
54178
- where: { seq: { gt: since } },
54269
+ where: { seq: { gt: since2 } },
54179
54270
  orderBy: { seq: "asc" },
54180
54271
  take: limit
54181
54272
  });
@@ -55366,12 +55457,12 @@ function requestRelay(deviceId, r, opts2 = {}) {
55366
55457
  return Promise.reject(new RelayError("too-large"));
55367
55458
  const id2 = `r${++counter}`;
55368
55459
  return new Promise((resolve24, reject2) => {
55369
- const timer10 = setTimeout(() => {
55460
+ const timer11 = setTimeout(() => {
55370
55461
  sendCancel(link2, id2);
55371
55462
  settle(link2, id2, new RelayError("timeout"));
55372
55463
  }, opts2.timeoutMs ?? RELAY_REQ_TIMEOUT_MS);
55373
- timer10.unref?.();
55374
- 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 });
55375
55466
  try {
55376
55467
  link2.socket.send(JSON.stringify({
55377
55468
  t: "req",
@@ -56694,8 +56785,8 @@ var init_store = __esm({
56694
56785
  async bindSession(chatId, sessionId2) {
56695
56786
  await this.db.telegramChat.update({ where: { chatId }, data: { sessionId: sessionId2 } });
56696
56787
  }
56697
- async rateLimitExceeded(userId, since, limit) {
56698
- 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;
56699
56790
  }
56700
56791
  async markUpdateUncertain(updateId, reason) {
56701
56792
  await this.db.telegramUpdate.updateMany({
@@ -56714,9 +56805,9 @@ var init_store = __esm({
56714
56805
  * SELAMANYA, dan tanpa pagar ini gateway akan mengetik selamanya sekaligus mengunci long-poll
56715
56806
  * di 4 detik selamanya.
56716
56807
  */
56717
- async chatsAwaitingReply(since) {
56808
+ async chatsAwaitingReply(since2) {
56718
56809
  const pending2 = await this.db.telegramUpdate.findMany({
56719
- where: { state: "dispatched", chatId: { not: null }, dispatchedAt: { gte: since } },
56810
+ where: { state: "dispatched", chatId: { not: null }, dispatchedAt: { gte: since2 } },
56720
56811
  select: { updateId: true, chatId: true },
56721
56812
  orderBy: { updateId: "asc" }
56722
56813
  });
@@ -57323,12 +57414,12 @@ function startPresenceSender(o) {
57323
57414
  heartbeatMs: o.heartbeatMs
57324
57415
  });
57325
57416
  void sender.tick(Date.now());
57326
- const timer10 = setInterval(() => {
57417
+ const timer11 = setInterval(() => {
57327
57418
  void sender.tick(Date.now());
57328
57419
  }, o.tickMs ?? PRESENCE_TICK_MS);
57329
- timer10.unref?.();
57420
+ timer11.unref?.();
57330
57421
  return { stop() {
57331
- clearInterval(timer10);
57422
+ clearInterval(timer11);
57332
57423
  } };
57333
57424
  }
57334
57425
  var init_sender = __esm({
@@ -57379,7 +57470,7 @@ function capabilityForRoute(method, path) {
57379
57470
  if (top === "devices") return "relay:exec";
57380
57471
  if (top === "logs") return rw("logs");
57381
57472
  if (top === "presence") return read2 ? "presence:read" : "COOKIE_ONLY";
57382
- 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")
57383
57474
  return read2 ? "GLOBAL_READ" : "COOKIE_ONLY";
57384
57475
  if (top === "session-results") return rw("activity");
57385
57476
  if (top === "portal-chat") return rw("portal-chat");
@@ -60996,7 +61087,7 @@ var require_stream = __commonJS({
60996
61087
  };
60997
61088
  duplex._final = function(callback) {
60998
61089
  if (ws2.readyState === ws2.CONNECTING) {
60999
- ws2.once("open", function open5() {
61090
+ ws2.once("open", function open6() {
61000
61091
  duplex._final(callback);
61001
61092
  });
61002
61093
  return;
@@ -61017,7 +61108,7 @@ var require_stream = __commonJS({
61017
61108
  };
61018
61109
  duplex._write = function(chunk, encoding, callback) {
61019
61110
  if (ws2.readyState === ws2.CONNECTING) {
61020
- ws2.once("open", function open5() {
61111
+ ws2.once("open", function open6() {
61021
61112
  duplex._write(chunk, encoding, callback);
61022
61113
  });
61023
61114
  return;
@@ -61749,14 +61840,14 @@ async function loadCursor(lane) {
61749
61840
  const c = await prisma.logCursor.findUnique({ where: { deviceId_lane: { deviceId: LOCAL_DEVICE_ID, lane } } });
61750
61841
  return c?.seq ?? 0n;
61751
61842
  }
61752
- async function nextBatch(lane, since) {
61843
+ async function nextBatch(lane, since2) {
61753
61844
  if (lane === "server") {
61754
61845
  const segs = await readSpoolSegments("server");
61755
61846
  const flat = segs.flatMap((s2) => s2.entries);
61756
61847
  return flat.slice(0, LOG_BATCH_MAX_ENTRIES);
61757
61848
  }
61758
61849
  const rows = await prisma.logEntry.findMany({
61759
- where: { deviceId: LOCAL_DEVICE_ID, lane, seq: { gt: since } },
61850
+ where: { deviceId: LOCAL_DEVICE_ID, lane, seq: { gt: since2 } },
61760
61851
  orderBy: { seq: "asc" },
61761
61852
  take: LOG_BATCH_MAX_ENTRIES
61762
61853
  });
@@ -61775,9 +61866,9 @@ async function nextBatch(lane, since) {
61775
61866
  async function shipLane(lane, transport) {
61776
61867
  const now = Date.now();
61777
61868
  if ((pausedUntil.get(lane) ?? 0) > now) return;
61778
- let since = await loadCursor(lane);
61869
+ let since2 = await loadCursor(lane);
61779
61870
  for (let i = 0; i < LOG_SHIP_MAX_BATCHES_PER_TICK; i++) {
61780
- const entries4 = await nextBatch(lane, since);
61871
+ const entries4 = await nextBatch(lane, since2);
61781
61872
  if (entries4.length === 0) return;
61782
61873
  const res = await transport("POST", "/api/sync/logs", { lane, entries: entries4 });
61783
61874
  if (res.status === 200) {
@@ -61788,7 +61879,7 @@ async function shipLane(lane, transport) {
61788
61879
  update: { seq: last2 },
61789
61880
  create: { deviceId: LOCAL_DEVICE_ID, lane, seq: last2 }
61790
61881
  });
61791
- since = last2;
61882
+ since2 = last2;
61792
61883
  if (lane === "server") {
61793
61884
  const segs = await readSpoolSegments("server");
61794
61885
  for (const s2 of segs) if (s2.entries.every((e) => BigInt(e.seq) <= last2)) await removeSpoolSegment(s2.file);
@@ -61797,7 +61888,7 @@ async function shipLane(lane, transport) {
61797
61888
  }
61798
61889
  if (res.status === 400) {
61799
61890
  await appendGap("rejected");
61800
- since = BigInt(entries4[entries4.length - 1].seq);
61891
+ since2 = BigInt(entries4[entries4.length - 1].seq);
61801
61892
  continue;
61802
61893
  }
61803
61894
  if (res.status === 404) {
@@ -62246,7 +62337,7 @@ async function startSyncClient(base3, token, tickMs2) {
62246
62337
  started = true;
62247
62338
  startRelayClient(base3, token);
62248
62339
  const transport = fetchTransport(base3, token);
62249
- const tick6 = () => syncTick(transport);
62340
+ const tick7 = () => syncTick(transport);
62250
62341
  const connectWs = async () => {
62251
62342
  const { WebSocket: WebSocket2 } = await Promise.resolve().then(() => (init_wrapper(), wrapper_exports));
62252
62343
  const wsUrl = base3.replace(/^http/, "ws").replace(/\/$/, "") + "/api/sync/ws?" + OPTIONAL_QUERY;
@@ -62254,14 +62345,14 @@ async function startSyncClient(base3, token, tickMs2) {
62254
62345
  ws = sock2;
62255
62346
  sock2.on("open", () => {
62256
62347
  reconnectDelay = 0;
62257
- void tick6();
62348
+ void tick7();
62258
62349
  presence = startPresenceSender({ send: (json) => sock2.send(json) });
62259
62350
  });
62260
62351
  sock2.on("message", async (raw) => {
62261
62352
  try {
62262
62353
  const msg = JSON.parse(raw.toString());
62263
62354
  if (msg.t !== "sync") return;
62264
- if (!await applyFeedFrame(msg)) void tick6();
62355
+ if (!await applyFeedFrame(msg)) void tick7();
62265
62356
  } catch {
62266
62357
  }
62267
62358
  });
@@ -62283,11 +62374,11 @@ async function startSyncClient(base3, token, tickMs2) {
62283
62374
  });
62284
62375
  };
62285
62376
  await bootstrapOnce(transport).catch(() => null);
62286
- await tick6();
62377
+ await tick7();
62287
62378
  void connectWs();
62288
62379
  const ms = tickMs2 && tickMs2 > 0 ? tickMs2 : 15e3;
62289
62380
  timer3 = setInterval(() => {
62290
- void tick6();
62381
+ void tick7();
62291
62382
  }, ms);
62292
62383
  timer3.unref?.();
62293
62384
  }
@@ -65497,14 +65588,14 @@ var require_secure_json_parse = __commonJS({
65497
65588
  }
65498
65589
  return filter(obj2, { protoAction, constructorAction, safe: options2 && options2.safe });
65499
65590
  }
65500
- function filter(obj2, { protoAction = "error", constructorAction = "error", safe: safe2 } = {}) {
65591
+ function filter(obj2, { protoAction = "error", constructorAction = "error", safe: safe3 } = {}) {
65501
65592
  let next = [obj2];
65502
65593
  while (next.length) {
65503
65594
  const nodes = next;
65504
65595
  next = [];
65505
65596
  for (const node of nodes) {
65506
65597
  if (protoAction !== "ignore" && Object.prototype.hasOwnProperty.call(node, "__proto__")) {
65507
- if (safe2 === true) {
65598
+ if (safe3 === true) {
65508
65599
  return null;
65509
65600
  } else if (protoAction === "error") {
65510
65601
  throw new SyntaxError("Object contains forbidden prototype property");
@@ -65512,7 +65603,7 @@ var require_secure_json_parse = __commonJS({
65512
65603
  delete node.__proto__;
65513
65604
  }
65514
65605
  if (constructorAction !== "ignore" && Object.prototype.hasOwnProperty.call(node, "constructor") && node.constructor !== null && typeof node.constructor === "object" && Object.prototype.hasOwnProperty.call(node.constructor, "prototype")) {
65515
- if (safe2 === true) {
65606
+ if (safe3 === true) {
65516
65607
  return null;
65517
65608
  } else if (constructorAction === "error") {
65518
65609
  throw new SyntaxError("Object contains forbidden prototype property");
@@ -66147,12 +66238,12 @@ var UploadError = class extends Error {
66147
66238
  };
66148
66239
  function timeout(promise, ms, code) {
66149
66240
  return new Promise((resolve24, reject2) => {
66150
- 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);
66151
66242
  promise.then((value) => {
66152
- clearTimeout(timer10);
66243
+ clearTimeout(timer11);
66153
66244
  resolve24(value);
66154
66245
  }, (error) => {
66155
- clearTimeout(timer10);
66246
+ clearTimeout(timer11);
66156
66247
  reject2(error);
66157
66248
  });
66158
66249
  });
@@ -66175,16 +66266,16 @@ function scannerFromEnv(path) {
66175
66266
  if (!isAbsolute8(command)) return Promise.reject(new UploadError("UPLOAD_SCAN", "scanner path must be absolute"));
66176
66267
  return new Promise((resolve24, reject2) => {
66177
66268
  const child = spawn2(command, [path], { shell: false, stdio: "ignore" });
66178
- const timer10 = setTimeout(() => {
66269
+ const timer11 = setTimeout(() => {
66179
66270
  child.kill("SIGKILL");
66180
66271
  reject2(new UploadError("UPLOAD_SCAN", "scanner timeout"));
66181
66272
  }, UPLOAD_LIMITS.scanMs);
66182
66273
  child.once("error", (error) => {
66183
- clearTimeout(timer10);
66274
+ clearTimeout(timer11);
66184
66275
  reject2(error);
66185
66276
  });
66186
66277
  child.once("exit", (code) => {
66187
- clearTimeout(timer10);
66278
+ clearTimeout(timer11);
66188
66279
  if (code === 0) resolve24();
66189
66280
  else reject2(new UploadError("UPLOAD_SCAN", `scanner exit ${code}`));
66190
66281
  });
@@ -66340,12 +66431,12 @@ async function addSpecAttachments(spec, files2) {
66340
66431
  continue;
66341
66432
  }
66342
66433
  const ext = attachmentExt(name2);
66343
- let safe2;
66434
+ let safe3;
66344
66435
  try {
66345
66436
  const image = IMAGE_TYPES[f.mime];
66346
66437
  if (image) {
66347
66438
  if (!image.includes(ext)) throw new UploadError("UPLOAD_TYPE", "extension mismatch");
66348
- safe2 = await processUpload({
66439
+ safe3 = await processUpload({
66349
66440
  buffer: f.buf,
66350
66441
  clientName: name2,
66351
66442
  clientMime: f.mime,
@@ -66353,7 +66444,7 @@ async function addSpecAttachments(spec, files2) {
66353
66444
  parentBytes: bytes
66354
66445
  });
66355
66446
  } else if (DOCUMENT_TYPES[f.mime]) {
66356
- safe2 = await processDocumentUpload({
66447
+ safe3 = await processDocumentUpload({
66357
66448
  buffer: f.buf,
66358
66449
  clientName: name2,
66359
66450
  clientMime: f.mime,
@@ -66372,17 +66463,17 @@ async function addSpecAttachments(spec, files2) {
66372
66463
  row = await prisma.specAttachment.create({ data: {
66373
66464
  specId: spec.id,
66374
66465
  projectId: spec.projectId,
66375
- filename: safe2.filename,
66376
- mimeType: safe2.mimeType,
66377
- size: safe2.size,
66378
- storageKey: safe2.storageKey
66466
+ filename: safe3.filename,
66467
+ mimeType: safe3.mimeType,
66468
+ size: safe3.size,
66469
+ storageKey: safe3.storageKey
66379
66470
  } });
66380
66471
  } catch (error) {
66381
- await deleteUpload(safe2.storageKey);
66472
+ await deleteUpload(safe3.storageKey);
66382
66473
  throw error;
66383
66474
  }
66384
66475
  count += 1;
66385
- bytes += safe2.size;
66476
+ bytes += safe3.size;
66386
66477
  saved.push(view2(row));
66387
66478
  }
66388
66479
  return { saved, rejected };
@@ -66590,14 +66681,14 @@ async function addQaAttachments(report, owner, files2) {
66590
66681
  continue;
66591
66682
  }
66592
66683
  const ext = attachmentExt(name2);
66593
- let safe2;
66684
+ let safe3;
66594
66685
  try {
66595
66686
  const image = IMAGE_TYPES[f.mime];
66596
66687
  if (image) {
66597
66688
  if (!image.includes(ext)) throw new UploadError("UPLOAD_TYPE", "extension mismatch");
66598
- 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 });
66599
66690
  } else if (DOCUMENT_TYPES[f.mime]) {
66600
- 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 });
66601
66692
  } else {
66602
66693
  throw new UploadError("UPLOAD_TYPE", "unsupported type");
66603
66694
  }
@@ -66607,28 +66698,28 @@ async function addQaAttachments(report, owner, files2) {
66607
66698
  continue;
66608
66699
  }
66609
66700
  try {
66610
- const sha256 = createHash6("sha256").update(await readUpload(safe2.storageKey)).digest("hex");
66701
+ const sha256 = createHash6("sha256").update(await readUpload(safe3.storageKey)).digest("hex");
66611
66702
  const row = await prisma.qaAttachment.create({ data: {
66612
66703
  reportId: report.id,
66613
66704
  projectId: report.projectId,
66614
66705
  ownerType: owner.ownerType,
66615
66706
  ownerId: owner.ownerId,
66616
- filename: safe2.filename,
66617
- mimeType: safe2.mimeType,
66618
- size: safe2.size,
66707
+ filename: safe3.filename,
66708
+ mimeType: safe3.mimeType,
66709
+ size: safe3.size,
66619
66710
  sha256,
66620
- storageKey: safe2.storageKey,
66711
+ storageKey: safe3.storageKey,
66621
66712
  // Client: byte baru ada di sini saja → menunggu unggah ke hub. Hub/standalone: tak ada atasan → langsung tersedia.
66622
66713
  syncState: effectiveStr("SYNC_SERVER_URL") ? "local-only" : "available"
66623
66714
  } });
66624
66715
  await notifySynced("qaAttachment", row.id);
66625
66716
  saved.push(attachmentView(row));
66626
66717
  } catch (error) {
66627
- await deleteUpload(safe2.storageKey);
66718
+ await deleteUpload(safe3.storageKey);
66628
66719
  throw error;
66629
66720
  }
66630
66721
  count += 1;
66631
- bytes += safe2.size;
66722
+ bytes += safe3.size;
66632
66723
  }
66633
66724
  return { saved, rejected };
66634
66725
  }
@@ -66964,9 +67055,9 @@ async function toProjectView(p3, sessions, devices2) {
66964
67055
  const specs = await prisma.spec.findMany({ where: { projectId: p3.id } });
66965
67056
  const { coverage } = await scanRepoDocs(await resolveRepoDir(p3.id));
66966
67057
  const binding = await getBinding(p3.id);
66967
- const open5 = specs.filter((s2) => s2.stage !== "done");
67058
+ const open6 = specs.filter((s2) => s2.stage !== "done");
66968
67059
  const { session, commit } = sessionOf(p3.id, sessions);
66969
- 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";
66970
67061
  return {
66971
67062
  id: p3.id,
66972
67063
  name: p3.name,
@@ -66979,7 +67070,7 @@ async function toProjectView(p3, sessions, devices2) {
66979
67070
  docStatus: docStatusFor(coverage),
66980
67071
  coverage,
66981
67072
  createdAt: p3.createdAt.toISOString(),
66982
- backlog: open5.length,
67073
+ backlog: open6.length,
66983
67074
  topStage,
66984
67075
  session,
66985
67076
  activity: session.status === "running" ? `running \xB7 ${session.flow ?? "sesi"}` : "idle",
@@ -68448,9 +68539,9 @@ async function deleteWorktrees(repoDir, names, opts2) {
68448
68539
  try {
68449
68540
  if (w.locked) await opts2.unlock(report.repoDir, w.path);
68450
68541
  if (w.session) {
68451
- const closed = await opts2.closeSession(w.session.id);
68542
+ const closed2 = await opts2.closeSession(w.session.id);
68452
68543
  row.closedSession = w.session.id;
68453
- row.cleanup = closed?.cleanup ?? null;
68544
+ row.cleanup = closed2?.cleanup ?? null;
68454
68545
  }
68455
68546
  if (!row.cleanup) row.cleanup = opts2.release(report.repoDir, w.path);
68456
68547
  await opts2.prune(report.repoDir);
@@ -68757,11 +68848,11 @@ async function beginSession(b) {
68757
68848
  });
68758
68849
  }
68759
68850
  async function finishSession(d) {
68760
- const open5 = await prisma.sessionHistory.findFirst({
68851
+ const open6 = await prisma.sessionHistory.findFirst({
68761
68852
  where: { sessionId: d.sessionId, endedAt: null },
68762
68853
  orderBy: { startedAt: "desc" }
68763
68854
  });
68764
- if (!open5) return;
68855
+ if (!open6) return;
68765
68856
  let t = { key: "", bytes: 0 };
68766
68857
  if (d.transcript) {
68767
68858
  try {
@@ -68771,7 +68862,7 @@ async function finishSession(d) {
68771
68862
  }
68772
68863
  }
68773
68864
  await prisma.sessionHistory.update({
68774
- where: { id: open5.id },
68865
+ where: { id: open6.id },
68775
68866
  data: {
68776
68867
  endedAt: /* @__PURE__ */ new Date(),
68777
68868
  endedReason: CLOSED,
@@ -68886,22 +68977,22 @@ async function reconcileTranscripts(opts2 = {}) {
68886
68977
  return report;
68887
68978
  }
68888
68979
  async function reconcileHistory(liveSessionIds) {
68889
- const open5 = await prisma.sessionHistory.findMany({
68980
+ const open6 = await prisma.sessionHistory.findMany({
68890
68981
  where: { endedAt: null },
68891
68982
  select: { id: true, sessionId: true, updatedAt: true }
68892
68983
  });
68893
68984
  const live = new Set(liveSessionIds);
68894
68985
  const at = /* @__PURE__ */ new Date();
68895
- let closed = 0;
68896
- for (const r of open5) {
68986
+ let closed2 = 0;
68987
+ for (const r of open6) {
68897
68988
  if (live.has(r.sessionId)) continue;
68898
68989
  await prisma.sessionHistory.update({
68899
68990
  where: { id: r.id },
68900
68991
  data: { endedAt: r.updatedAt, endedReason: RECONCILED, reconciledAt: at }
68901
68992
  });
68902
- closed++;
68993
+ closed2++;
68903
68994
  }
68904
- return closed;
68995
+ return closed2;
68905
68996
  }
68906
68997
  async function reconciledSpecIdsSince(cutoff) {
68907
68998
  const rows = await prisma.sessionHistory.findMany({
@@ -68973,13 +69064,13 @@ async function detectOrphanWorktrees(deps = { repos: prodReaperDeps.repos, input
68973
69064
  // src/services/worktree-delete.ts
68974
69065
  var execAsync = promisify11(execFile12);
68975
69066
  async function lockInputs(id2) {
68976
- const open5 = await prisma.spec.findMany({
69067
+ const open6 = await prisma.spec.findMany({
68977
69068
  where: { projectId: id2, stage: { not: "done" } },
68978
69069
  select: { id: true }
68979
69070
  });
68980
69071
  const sessions = listSessions().filter((s2) => s2.projectId === id2 && !s2.exited).map((s2) => s2.branch || (s2.specId ? `hanoman/${s2.id}` : "")).filter(Boolean);
68981
69072
  return {
68982
- openSpecBranches: new Set(open5.map((s2) => sourceBranch(s2.id))),
69073
+ openSpecBranches: new Set(open6.map((s2) => sourceBranch(s2.id))),
68983
69074
  sessionBranches: new Set(sessions)
68984
69075
  };
68985
69076
  }
@@ -74246,7 +74337,7 @@ function createBoundedSender(ws2, o = {}) {
74246
74337
  const drainMs = o.drainMs ?? DRAIN_MS;
74247
74338
  let pending2 = [];
74248
74339
  let pendingBytes = 0;
74249
- let timer10;
74340
+ let timer11;
74250
74341
  let resyncNeeded = false;
74251
74342
  const trim = () => {
74252
74343
  for (let i = 0; i < pending2.length && pendingBytes > cap; ) {
@@ -74260,10 +74351,10 @@ function createBoundedSender(ws2, o = {}) {
74260
74351
  }
74261
74352
  };
74262
74353
  const drain2 = () => {
74263
- timer10 = void 0;
74354
+ timer11 = void 0;
74264
74355
  if (ws2.bufferedAmount > high) {
74265
- timer10 = setTimeout(drain2, drainMs);
74266
- timer10.unref?.();
74356
+ timer11 = setTimeout(drain2, drainMs);
74357
+ timer11.unref?.();
74267
74358
  return;
74268
74359
  }
74269
74360
  const out4 = pending2;
@@ -74284,14 +74375,14 @@ function createBoundedSender(ws2, o = {}) {
74284
74375
  pending2.push(frame2);
74285
74376
  pendingBytes += frame2.length;
74286
74377
  trim();
74287
- if (!timer10) {
74288
- timer10 = setTimeout(drain2, drainMs);
74289
- timer10.unref?.();
74378
+ if (!timer11) {
74379
+ timer11 = setTimeout(drain2, drainMs);
74380
+ timer11.unref?.();
74290
74381
  }
74291
74382
  },
74292
74383
  dispose() {
74293
- if (timer10) clearTimeout(timer10);
74294
- timer10 = void 0;
74384
+ if (timer11) clearTimeout(timer11);
74385
+ timer11 = void 0;
74295
74386
  pending2 = [];
74296
74387
  pendingBytes = 0;
74297
74388
  }
@@ -74439,7 +74530,7 @@ function startSessionEventRelay(app3, options2 = {}) {
74439
74530
  const root = options2.root ?? sessionEventSpoolRoot();
74440
74531
  const host2 = controlHost(loadIngressPolicy(options2.env ?? process.env));
74441
74532
  let running = false;
74442
- const tick6 = async () => {
74533
+ const tick7 = async () => {
74443
74534
  if (running) return;
74444
74535
  running = true;
74445
74536
  try {
@@ -74450,14 +74541,14 @@ function startSessionEventRelay(app3, options2 = {}) {
74450
74541
  running = false;
74451
74542
  }
74452
74543
  };
74453
- const timer10 = setInterval(() => {
74454
- void tick6();
74544
+ const timer11 = setInterval(() => {
74545
+ void tick7();
74455
74546
  }, options2.intervalMs ?? 250);
74456
- timer10.unref();
74547
+ timer11.unref();
74457
74548
  app3.addHook("onClose", async () => {
74458
- clearInterval(timer10);
74549
+ clearInterval(timer11);
74459
74550
  });
74460
- void tick6();
74551
+ void tick7();
74461
74552
  }
74462
74553
 
74463
74554
  // src/services/agent-invocations.ts
@@ -74681,10 +74772,10 @@ async function stopAgentInvocation(input, io = {}) {
74681
74772
  }
74682
74773
  async function reconcileAgentInvocations(liveSessionIds) {
74683
74774
  const live = new Set(liveSessionIds);
74684
- const open5 = await prisma.agentInvocation.findMany({ where: { status: "running" } });
74775
+ const open6 = await prisma.agentInvocation.findMany({ where: { status: "running" } });
74685
74776
  let changed = 0;
74686
74777
  const endedAt = /* @__PURE__ */ new Date();
74687
- for (const row of open5) {
74778
+ for (const row of open6) {
74688
74779
  if (live.has(row.sessionId)) continue;
74689
74780
  await prisma.agentInvocation.update({
74690
74781
  where: { id: row.id },
@@ -75080,8 +75171,8 @@ async function expireFlows(now) {
75080
75171
  });
75081
75172
  const out4 = [];
75082
75173
  for (const f of due) {
75083
- const closed = await closeFlow(f.id, "kedaluwarsa");
75084
- if (closed) out4.push(closed);
75174
+ const closed2 = await closeFlow(f.id, "kedaluwarsa");
75175
+ if (closed2) out4.push(closed2);
75085
75176
  }
75086
75177
  return out4;
75087
75178
  }
@@ -76768,7 +76859,7 @@ async function terminal_default(app3, opts2) {
76768
76859
  });
76769
76860
  const client = { send: (m) => bounded.send(m), close: () => socket2.close() };
76770
76861
  let ready = false;
76771
- let closed = false;
76862
+ let closed2 = false;
76772
76863
  const early = [];
76773
76864
  const attachReady = (async () => {
76774
76865
  const found = await getSessionAsync(id2);
@@ -76776,9 +76867,9 @@ async function terminal_default(app3, opts2) {
76776
76867
  socket2.close(4004, "not found");
76777
76868
  return;
76778
76869
  }
76779
- if (closed) return;
76870
+ if (closed2) return;
76780
76871
  await attachAsync(id2, client);
76781
- if (closed) {
76872
+ if (closed2) {
76782
76873
  detach(id2, client);
76783
76874
  return;
76784
76875
  }
@@ -76826,7 +76917,7 @@ async function terminal_default(app3, opts2) {
76826
76917
  const revalidate = watch ? setInterval(() => watch.refresh(), 6e4) : void 0;
76827
76918
  revalidate?.unref?.();
76828
76919
  socket2.on("close", () => {
76829
- closed = true;
76920
+ closed2 = true;
76830
76921
  bounded.dispose();
76831
76922
  if (revalidate) clearInterval(revalidate);
76832
76923
  watch?.dispose();
@@ -77184,9 +77275,9 @@ function sshExec(t, remoteCmd, opts2 = {}) {
77184
77275
  return new Promise((resolve24) => {
77185
77276
  const p3 = spawn4(sshBin(), args, { stdio: ["pipe", "pipe", "pipe"], env });
77186
77277
  let out4 = "";
77187
- const timer10 = setTimeout(() => p3.kill("SIGKILL"), opts2.timeoutMs ?? 6e4);
77278
+ const timer11 = setTimeout(() => p3.kill("SIGKILL"), opts2.timeoutMs ?? 6e4);
77188
77279
  const done = (r) => {
77189
- clearTimeout(timer10);
77280
+ clearTimeout(timer11);
77190
77281
  if (askpass) rmSync12(dirname15(askpass), { recursive: true, force: true });
77191
77282
  resolve24(r);
77192
77283
  };
@@ -78258,7 +78349,7 @@ function runCatalogCommand(command, agent, cwd, env, timeoutMs = TIMEOUT_MS) {
78258
78349
  const finish = (error, result) => {
78259
78350
  if (settled) return;
78260
78351
  settled = true;
78261
- clearTimeout(timer10);
78352
+ clearTimeout(timer11);
78262
78353
  child.stdin.end();
78263
78354
  if (child.exitCode === null && child.signalCode === null) {
78264
78355
  child.kill("SIGTERM");
@@ -78268,7 +78359,7 @@ function runCatalogCommand(command, agent, cwd, env, timeoutMs = TIMEOUT_MS) {
78268
78359
  if (error) reject2(error);
78269
78360
  else resolve24(result);
78270
78361
  };
78271
- 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);
78272
78363
  child.on("error", () => finish(new Error("CLI model tidak dapat dijalankan")));
78273
78364
  child.stdin.on("error", () => finish(new Error("Kanal katalog CLI terputus")));
78274
78365
  child.stderr.on("data", () => {
@@ -78409,11 +78500,11 @@ var modelCatalogService = createModelCatalogService({
78409
78500
  });
78410
78501
  function startModelDiscovery() {
78411
78502
  void modelCatalogService.refresh();
78412
- const timer10 = setInterval(() => {
78503
+ const timer11 = setInterval(() => {
78413
78504
  void modelCatalogService.refresh();
78414
78505
  }, 5 * 6e4);
78415
- timer10.unref();
78416
- return () => clearInterval(timer10);
78506
+ timer11.unref();
78507
+ return () => clearInterval(timer11);
78417
78508
  }
78418
78509
 
78419
78510
  // src/routes/models.ts
@@ -78438,18 +78529,18 @@ function requestConfigRestart(delayMs = 50) {
78438
78529
  }
78439
78530
  function afterReplyFlushed(raw, fn, maxWaitMs = 5e3) {
78440
78531
  let done = false;
78441
- let timer10;
78532
+ let timer11;
78442
78533
  const run5 = () => {
78443
78534
  if (done) return;
78444
78535
  done = true;
78445
- if (timer10) clearTimeout(timer10);
78536
+ if (timer11) clearTimeout(timer11);
78446
78537
  raw.removeListener("finish", run5);
78447
78538
  raw.removeListener("close", run5);
78448
78539
  fn();
78449
78540
  };
78450
78541
  raw.once("finish", run5);
78451
78542
  raw.once("close", run5);
78452
- timer10 = setTimeout(run5, maxWaitMs);
78543
+ timer11 = setTimeout(run5, maxWaitMs);
78453
78544
  }
78454
78545
  function requestManualRestart(delayMs = 250) {
78455
78546
  setTimeout(() => process.exit(MANUAL_RESTART_EXIT), delayMs);
@@ -78516,6 +78607,364 @@ async function pendingCounts(now = Date.now(), prds = listAllPrds) {
78516
78607
  return { ...EMPTY_PENDING, triage: tickets2 + issues, backlog, prd, lead };
78517
78608
  }
78518
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
+
78519
78968
  // src/services/events.ts
78520
78969
  init_db();
78521
78970
  init_config2();
@@ -79298,11 +79747,11 @@ var HOUSEKEEPING_MS = 6e4;
79298
79747
  var busyPulse = false;
79299
79748
  var lastPulseAt = 0;
79300
79749
  var pulseEndedAt = 0;
79301
- var timer4;
79750
+ var timer5;
79302
79751
  function lastPulse() {
79303
79752
  return lastPulseAt;
79304
79753
  }
79305
- async function tick(now, deps = {}) {
79754
+ async function tick2(now, deps = {}) {
79306
79755
  let cfg;
79307
79756
  try {
79308
79757
  cfg = await (deps.cfg ?? getLead)();
@@ -79353,10 +79802,10 @@ async function tick(now, deps = {}) {
79353
79802
  await Promise.all(jobs);
79354
79803
  }
79355
79804
  function startLead(deps = {}) {
79356
- if (timer4) return;
79357
- timer4 = setInterval(() => void tick(Date.now(), deps), HOUSEKEEPING_MS);
79358
- timer4.unref();
79359
- 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);
79360
79809
  }
79361
79810
 
79362
79811
  // src/services/lead/views.ts
@@ -79367,7 +79816,7 @@ async function buildLeadStatus() {
79367
79816
  select: { id: true, name: true },
79368
79817
  orderBy: { id: "asc" }
79369
79818
  });
79370
- const since = new Date(Date.now() - 24 * 60 * 60 * 1e3);
79819
+ const since2 = new Date(Date.now() - 24 * 60 * 60 * 1e3);
79371
79820
  let live = [];
79372
79821
  try {
79373
79822
  live = (await listSessionsAsync()).filter((s2) => !s2.exited);
@@ -79383,7 +79832,7 @@ async function buildLeadStatus() {
79383
79832
  name: p3.name,
79384
79833
  optIn: true,
79385
79834
  paused: !leadActive(cfg, p3.id),
79386
- 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 } } }),
79387
79836
  openSessions: live.filter((s2) => s2.projectId === p3.id).length
79388
79837
  })));
79389
79838
  const last2 = lastPulse();
@@ -79555,7 +80004,16 @@ var GROUPS = [
79555
80004
  // bukan empat grup: keempatnya berubah dari peristiwa yang sama (item diajukan/diputuskan) dan
79556
80005
  // dibaca satu komponen yang sama. `cookieOnly` TIDAK dipasang — muatannya jumlah agregat tanpa
79557
80006
  // id/judul, dan agent token sudah boleh membaca daftar sumbernya lewat capability masing-masing.
79558
- { 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
+ }
79559
80017
  ];
79560
80018
  function broadcast2(msg, s2, cookieOnly = false) {
79561
80019
  for (const c of clients) {
@@ -79642,9 +80100,9 @@ function subscribeClient(c, subs) {
79642
80100
  if (e.subscribers.size === 0) entries2.delete(key);
79643
80101
  }
79644
80102
  }
79645
- var tick2 = 0;
80103
+ var tick3 = 0;
79646
80104
  var busy = false;
79647
- var timer5;
80105
+ var timer6;
79648
80106
  async function runGroup(g) {
79649
80107
  const t0 = profStart();
79650
80108
  let msg;
@@ -79684,9 +80142,9 @@ async function runGroup(g) {
79684
80142
  async function __tick() {
79685
80143
  if (busy) return;
79686
80144
  busy = true;
79687
- tick2++;
80145
+ tick3++;
79688
80146
  try {
79689
- 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));
79690
80148
  for (const r of results) {
79691
80149
  if (!r) continue;
79692
80150
  r.g.last = r.json;
@@ -79704,19 +80162,19 @@ async function __tick() {
79704
80162
  }
79705
80163
  }
79706
80164
  function startLoop() {
79707
- if (timer5) return;
80165
+ if (timer6) return;
79708
80166
  startLagMonitor();
79709
- timer5 = setInterval(() => {
80167
+ timer6 = setInterval(() => {
79710
80168
  void __tick();
79711
80169
  }, tickMs());
79712
- timer5.unref();
80170
+ timer6.unref();
79713
80171
  }
79714
80172
  function stopLoop() {
79715
- if (timer5) {
79716
- clearInterval(timer5);
79717
- timer5 = void 0;
80173
+ if (timer6) {
80174
+ clearInterval(timer6);
80175
+ timer6 = void 0;
79718
80176
  }
79719
- tick2 = 0;
80177
+ tick3 = 0;
79720
80178
  for (const g of GROUPS) {
79721
80179
  g.last = "";
79722
80180
  g.lastSig = void 0;
@@ -80361,6 +80819,11 @@ async function presence_default(app3) {
80361
80819
  app3.get("/presence", async () => presenceView());
80362
80820
  }
80363
80821
 
80822
+ // src/routes/overview.ts
80823
+ async function overview_default(app3) {
80824
+ app3.get("/overview", async () => getOverview());
80825
+ }
80826
+
80364
80827
  // src/routes/session-results.ts
80365
80828
  init_db();
80366
80829
  var view6 = (r) => ({
@@ -80671,7 +81134,7 @@ async function intakeTicket(input) {
80671
81134
  let ticketBytes = 0;
80672
81135
  for (const f of input.files) {
80673
81136
  try {
80674
- const safe2 = await processUpload({
81137
+ const safe3 = await processUpload({
80675
81138
  buffer: f.buf,
80676
81139
  clientName: f.name,
80677
81140
  clientMime: f.mime,
@@ -80684,17 +81147,17 @@ async function intakeTicket(input) {
80684
81147
  data: {
80685
81148
  ticketId: ticket.id,
80686
81149
  projectId: input.projectId,
80687
- filename: safe2.filename,
80688
- mimeType: safe2.mimeType,
80689
- size: safe2.size,
80690
- storageKey: safe2.storageKey
81150
+ filename: safe3.filename,
81151
+ mimeType: safe3.mimeType,
81152
+ size: safe3.size,
81153
+ storageKey: safe3.storageKey
80691
81154
  }
80692
81155
  });
80693
81156
  } catch (error) {
80694
- await deleteUpload(safe2.storageKey);
81157
+ await deleteUpload(safe3.storageKey);
80695
81158
  throw error;
80696
81159
  }
80697
- ticketBytes += safe2.size;
81160
+ ticketBytes += safe3.size;
80698
81161
  await notifySynced("ticketAttachment", att.id);
80699
81162
  } catch (error) {
80700
81163
  if (!(error instanceof UploadError)) throw error;
@@ -82714,7 +83177,7 @@ var QaImportError = class extends Error {
82714
83177
  this.status = status2;
82715
83178
  }
82716
83179
  };
82717
- var safe = (s2) => s2.replace(/[^A-Za-z0-9._-]/g, "_");
83180
+ var safe2 = (s2) => s2.replace(/[^A-Za-z0-9._-]/g, "_");
82718
83181
  var MIME_BY_EXT = {
82719
83182
  png: "image/png",
82720
83183
  jpg: "image/jpeg",
@@ -82742,7 +83205,7 @@ async function exportReport(projectId, reportId) {
82742
83205
  const owner = codeOf(a.ownerType, a.ownerId);
82743
83206
  const n2 = (seq.get(owner) ?? 0) + 1;
82744
83207
  seq.set(owner, n2);
82745
- paths2[a.id] = `attachments/${owner}-${n2}-${safe(a.filename)}`;
83208
+ paths2[a.id] = `attachments/${owner}-${n2}-${safe2(a.filename)}`;
82746
83209
  files2.push({ name: paths2[a.id], data });
82747
83210
  present.push(a);
82748
83211
  }
@@ -84168,7 +84631,7 @@ async function testTelegramConnection(deps = {}) {
84168
84631
  }
84169
84632
  const timeoutMs = deps.timeoutMs ?? 1e4;
84170
84633
  const controller = new AbortController();
84171
- const timer10 = setTimeout(() => controller.abort(), timeoutMs);
84634
+ const timer11 = setTimeout(() => controller.abort(), timeoutMs);
84172
84635
  const send = deps.transport ?? ((url2, init) => fetch(url2, init));
84173
84636
  const transport = (url2, init) => send(url2, { ...init, signal: controller.signal });
84174
84637
  const client = new TelegramApiClient(botToken, transport);
@@ -84182,7 +84645,7 @@ async function testTelegramConnection(deps = {}) {
84182
84645
  const message = controller.signal.aborted ? `Timeout ${timeoutMs} ms \u2014 Telegram tidak menjawab.` : raw;
84183
84646
  return { ok: false, inbound, error: sanitizeTelegramOutput(message, [botToken]).slice(0, 500) };
84184
84647
  } finally {
84185
- clearTimeout(timer10);
84648
+ clearTimeout(timer11);
84186
84649
  }
84187
84650
  }
84188
84651
  function resolveTestChatId() {
@@ -84416,7 +84879,7 @@ async function sendOnce(o, deps = {}) {
84416
84879
  nowSec: o.nowSec ?? Math.floor(Date.now() / 1e3)
84417
84880
  });
84418
84881
  const ac = new AbortController();
84419
- const timer10 = setTimeout(() => ac.abort(), WEBHOOK_TIMEOUT_MS);
84882
+ const timer11 = setTimeout(() => ac.abort(), WEBHOOK_TIMEOUT_MS);
84420
84883
  try {
84421
84884
  const res = deps.fetcher ? await deps.fetcher(parsed.url.toString(), {
84422
84885
  method: "POST",
@@ -84450,7 +84913,7 @@ async function sendOnce(o, deps = {}) {
84450
84913
  const msg = e.name === "AbortError" ? `timeout ${WEBHOOK_TIMEOUT_MS} ms` : e.message;
84451
84914
  return fail4(msg);
84452
84915
  } finally {
84453
- clearTimeout(timer10);
84916
+ clearTimeout(timer11);
84454
84917
  }
84455
84918
  }
84456
84919
 
@@ -84845,10 +85308,10 @@ function sanitizeClientText(text2) {
84845
85308
  return text2.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, "").slice(0, MAX_PESAN);
84846
85309
  }
84847
85310
  function wrapClientMessage(text2, nonce) {
84848
- const open5 = `<pesan-klien-${nonce}>`;
85311
+ const open6 = `<pesan-klien-${nonce}>`;
84849
85312
  const close = `</pesan-klien-${nonce}>`;
84850
85313
  const jinak = sanitizeClientText(text2).replaceAll("</pesan-klien", "<\u200B/pesan-klien").replaceAll("<pesan-klien", "<\u200Bpesan-klien");
84851
- return `${open5}
85314
+ return `${open6}
84852
85315
  ${jinak}
84853
85316
  ${close}`;
84854
85317
  }
@@ -86736,6 +87199,7 @@ function buildApp({ requireAuth = true, agentDocFile, env = process.env } = {})
86736
87199
  await api.register(bindings_default);
86737
87200
  await api.register(sync_default);
86738
87201
  await api.register(presence_default);
87202
+ await api.register(overview_default);
86739
87203
  await api.register(session_results_default);
86740
87204
  await api.register(session_history_default);
86741
87205
  await api.register(session_events_default);
@@ -86781,17 +87245,17 @@ function createShutdown(o) {
86781
87245
  let running;
86782
87246
  return (sig) => running ??= (async () => {
86783
87247
  log(`${sig} \u2014 menutup`);
86784
- const timer10 = setTimeout(() => {
87248
+ const timer11 = setTimeout(() => {
86785
87249
  log(`penutupan melewati ${o.timeoutMs ?? DEFAULT_TIMEOUT_MS} ms \u2014 keluar paksa`);
86786
87250
  o.exit(1);
86787
87251
  }, o.timeoutMs ?? DEFAULT_TIMEOUT_MS);
86788
- timer10.unref?.();
87252
+ timer11.unref?.();
86789
87253
  try {
86790
87254
  await o.close();
86791
87255
  } catch (e) {
86792
87256
  log(`penutupan gagal: ${e.message}`);
86793
87257
  }
86794
- clearTimeout(timer10);
87258
+ clearTimeout(timer11);
86795
87259
  o.exit(0);
86796
87260
  })();
86797
87261
  }
@@ -87056,7 +87520,7 @@ var prodEnd = {
87056
87520
  reconcile: () => reconcile(reconcileProdDeps),
87057
87521
  scanDecisions: () => scanDecisions()
87058
87522
  };
87059
- async function tick3(now, deps, end2 = prodEnd) {
87523
+ async function tick4(now, deps, end2 = prodEnd) {
87060
87524
  const cfg = await getScheduler();
87061
87525
  if (!cfg.enabled) return;
87062
87526
  for (const src of listSources()) {
@@ -87121,13 +87585,13 @@ var prodDeps = {
87121
87585
  launchCron: async (cron) => (await startCronSession(cron)).id
87122
87586
  })
87123
87587
  };
87124
- var TICK_MS2 = 1e4;
87125
- var timer6;
87588
+ var TICK_MS3 = 1e4;
87589
+ var timer7;
87126
87590
  function startScheduler(deps = prodDeps) {
87127
- if (timer6) return;
87128
- timer6 = setInterval(() => void tick3(Date.now(), deps), TICK_MS2);
87129
- timer6.unref();
87130
- 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);
87131
87595
  }
87132
87596
 
87133
87597
  // src/services/scheduler/sources/backlog.ts
@@ -87299,7 +87763,7 @@ async function installWebhooks() {
87299
87763
  // src/services/webhooks/engine.ts
87300
87764
  init_src();
87301
87765
  init_db();
87302
- var TICK_MS3 = 2e3;
87766
+ var TICK_MS4 = 2e3;
87303
87767
  var MAX_IN_FLIGHT = 4;
87304
87768
  var PRUNE_EVERY_TICKS = 30;
87305
87769
  function backoffAt(from, attempt) {
@@ -87442,15 +87906,15 @@ async function pruneHistory() {
87442
87906
  }
87443
87907
  return removed;
87444
87908
  }
87445
- var timer7;
87446
- var ticks = 0;
87909
+ var timer8;
87910
+ var ticks2 = 0;
87447
87911
  var busy2 = false;
87448
- async function tick4() {
87912
+ async function tick5() {
87449
87913
  if (busy2) return;
87450
87914
  busy2 = true;
87451
87915
  try {
87452
87916
  await deliverDue(/* @__PURE__ */ new Date());
87453
- if (++ticks % PRUNE_EVERY_TICKS === 0) await pruneHistory();
87917
+ if (++ticks2 % PRUNE_EVERY_TICKS === 0) await pruneHistory();
87454
87918
  } catch (e) {
87455
87919
  console.error("webhook engine:", e);
87456
87920
  } finally {
@@ -87458,9 +87922,9 @@ async function tick4() {
87458
87922
  }
87459
87923
  }
87460
87924
  function startWebhookEngine() {
87461
- if (timer7) return;
87462
- timer7 = setInterval(() => void tick4(), TICK_MS3);
87463
- timer7.unref();
87925
+ if (timer8) return;
87926
+ timer8 = setInterval(() => void tick5(), TICK_MS4);
87927
+ timer8.unref();
87464
87928
  void resetStuckDeliveries().then((n2) => {
87465
87929
  if (n2) console.log(`webhook: ${n2} pengiriman tertinggal dikembalikan ke antrean`);
87466
87930
  }).catch((e) => console.error("webhook reset:", e));
@@ -87475,7 +87939,7 @@ init_local_binding();
87475
87939
  init_notifications2();
87476
87940
  var AUTO_MERGE_WINDOW_MS = 24 * 60 * 60 * 1e3;
87477
87941
  var AUTO_MERGE_GRACE_MS = 15 * 60 * 1e3;
87478
- var TICK_MS4 = 6e4;
87942
+ var TICK_MS5 = 6e4;
87479
87943
  var exec11 = promisify17(execFile20);
87480
87944
  var GIT8 = { timeout: 3e4, maxBuffer: 1 << 24, encoding: "utf8" };
87481
87945
  var gitOut = async (cwd, args) => {
@@ -87498,9 +87962,9 @@ var prodAutoMergeDeps = {
87498
87962
  deleteBranch: deleteMergedBranch
87499
87963
  };
87500
87964
  async function candidates(now) {
87501
- const since = new Date(now.getTime() - AUTO_MERGE_WINDOW_MS);
87965
+ const since2 = new Date(now.getTime() - AUTO_MERGE_WINDOW_MS);
87502
87966
  const done = await prisma.notification.findMany({
87503
- where: { type: "done", createdAt: { gte: since }, specId: { not: null } },
87967
+ where: { type: "done", createdAt: { gte: since2 }, specId: { not: null } },
87504
87968
  select: { specId: true, projectId: true, createdAt: true }
87505
87969
  });
87506
87970
  if (!done.length) return [];
@@ -87564,9 +88028,9 @@ async function settleOne(c, deps, now) {
87564
88028
  }
87565
88029
  return report(`Auto-merge ${spec.id} \u2192 ${target3} GAGAL: ${res.error}`);
87566
88030
  }
87567
- var timer8;
88031
+ var timer9;
87568
88032
  var busy3 = false;
87569
- async function tick5() {
88033
+ async function tick6() {
87570
88034
  if (busy3) return;
87571
88035
  busy3 = true;
87572
88036
  try {
@@ -87578,9 +88042,9 @@ async function tick5() {
87578
88042
  }
87579
88043
  }
87580
88044
  function startAutoMerge() {
87581
- if (timer8) return;
87582
- timer8 = setInterval(() => void tick5(), TICK_MS4);
87583
- timer8.unref();
88045
+ if (timer9) return;
88046
+ timer9 = setInterval(() => void tick6(), TICK_MS5);
88047
+ timer9.unref();
87584
88048
  }
87585
88049
 
87586
88050
  // src/server.ts
@@ -87879,9 +88343,9 @@ async function pruneSyncFeed(now, dryRun = false) {
87879
88343
  }
87880
88344
  return pruned;
87881
88345
  }
87882
- var timer9;
88346
+ var timer10;
87883
88347
  function startRetentionSweep() {
87884
- if (timer9) return;
88348
+ if (timer10) return;
87885
88349
  const sweep = () => {
87886
88350
  const holds = new Set((process.env.HANOMAN_RETENTION_HOLDS ?? "").split(",").map((v) => v.trim()).filter(Boolean));
87887
88351
  void runRetention({ holds }).then((report) => {
@@ -87891,8 +88355,8 @@ function startRetentionSweep() {
87891
88355
  }).catch((error) => console.error("retention sweep:", error));
87892
88356
  };
87893
88357
  sweep();
87894
- timer9 = setInterval(sweep, 24 * 60 * 6e4);
87895
- timer9.unref?.();
88358
+ timer10 = setInterval(sweep, 24 * 60 * 6e4);
88359
+ timer10.unref?.();
87896
88360
  }
87897
88361
 
87898
88362
  // src/server.ts
@@ -87992,6 +88456,7 @@ bootstrapReady.then(async () => {
87992
88456
  startAutoMerge();
87993
88457
  startWorktreeReaper();
87994
88458
  startRetentionSweep();
88459
+ startOverviewRecorder();
87995
88460
  }).catch((err) => {
87996
88461
  console.error("listen gagal:", err);
87997
88462
  process.exit(1);