haansi 0.1.30 → 0.1.32

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 (2) hide show
  1. package/dist/haansi.js +407 -110
  2. package/package.json +1 -1
package/dist/haansi.js CHANGED
@@ -95,7 +95,7 @@ var require_package = __commonJS({
95
95
  "package.json"(exports2, module2) {
96
96
  module2.exports = {
97
97
  name: "haansi",
98
- version: "0.1.30",
98
+ version: "0.1.32",
99
99
  description: "Haansi CLI - Session collector and MCP server for Claude Code & OpenAI Codex",
100
100
  bin: {
101
101
  haansi: "./dist/haansi.js"
@@ -1203,16 +1203,37 @@ var init_metrics = __esm({
1203
1203
  MetricsService = class {
1204
1204
  projectId;
1205
1205
  serviceName;
1206
- isLocal;
1207
1206
  constructor(serviceName, projectId) {
1208
1207
  this.serviceName = serviceName;
1209
1208
  this.projectId = projectId === null ? "" : projectId || process.env.GCP_PROJECT_ID || "";
1210
- this.isLocal = process.env.NODE_ENV === "development" || !this.projectId;
1211
- if (!this.isLocal && !getCloudBackend2()) {
1212
- console.error(
1213
- `[MetricsService] WARNING: No cloud metrics backend registered for service "${serviceName}". Metrics will be logged to console only. Register a cloud provider to enable cloud metrics.`
1214
- );
1215
- }
1209
+ }
1210
+ /**
1211
+ * Whether to log to the console instead of a cloud backend.
1212
+ *
1213
+ * TWO BUGS LIVED IN THE OLD VERSION OF THIS, and both silently discarded
1214
+ * every metric on AWS.
1215
+ *
1216
+ * It read `NODE_ENV === "development" || !projectId`, where `projectId`
1217
+ * falls back to `GCP_PROJECT_ID`. On AWS that variable is unset — so a
1218
+ * production service classified itself as local and logged metrics to
1219
+ * stdout, where nothing could alarm or autoscale on them. The GCP-shaped
1220
+ * question ("do I have a project id?") had become a proxy for "am I in the
1221
+ * cloud?" the moment a second provider existed.
1222
+ *
1223
+ * And it was evaluated ONCE, in the constructor. Services construct
1224
+ * MetricsService at module load and register their cloud provider during
1225
+ * startup, so even on GCP the answer could be computed before the backend
1226
+ * that would have answered it existed.
1227
+ *
1228
+ * Now it asks the question that actually matters, at the moment it matters:
1229
+ * is there a registered backend to send to? A registered backend means
1230
+ * cloud, whatever any environment variable says. Development still forces
1231
+ * console so a local run never writes to a real metric namespace.
1232
+ */
1233
+ get isLocal() {
1234
+ if (process.env.NODE_ENV === "development") return true;
1235
+ if (getCloudBackend2()) return false;
1236
+ return !this.projectId;
1216
1237
  }
1217
1238
  /**
1218
1239
  * Record a custom metric
@@ -1651,6 +1672,110 @@ var init_observability_service = __esm({
1651
1672
  }
1652
1673
  });
1653
1674
 
1675
+ // ../../packages/@company/observability/src/scaling-metrics.ts
1676
+ function spec(s) {
1677
+ return s;
1678
+ }
1679
+ var SCALING_METRICS_SERVICE, SCALING_METRICS_NAMESPACE, SCALING_METRICS;
1680
+ var init_scaling_metrics = __esm({
1681
+ "../../packages/@company/observability/src/scaling-metrics.ts"() {
1682
+ "use strict";
1683
+ init_types();
1684
+ SCALING_METRICS_SERVICE = "backlog-reconciler";
1685
+ SCALING_METRICS_NAMESPACE = `Haansi/${SCALING_METRICS_SERVICE}`;
1686
+ SCALING_METRICS = {
1687
+ CANONICAL_BACKLOG_EVENTS: spec({
1688
+ name: "haansi/ingestion/canonical_backlog_events",
1689
+ unit: "Count",
1690
+ description: "source_events rows awaiting normalization (normalized_at IS NULL), all orgs.",
1691
+ dimensions: ["environment"],
1692
+ treatMissingData: "breaching",
1693
+ missingDataRationale: "The reconciler is the only publisher and runs on a schedule independent of every worker. Its silence means the reconciler itself is broken, and the backlog it was watching is unobserved rather than empty. Scaling out against an unknown backlog wastes money; scaling in loses the queue. Breaching keeps capacity up until a human looks."
1694
+ }),
1695
+ CANONICAL_BACKLOG_OLDEST_SECONDS: spec({
1696
+ name: "haansi/ingestion/canonical_backlog_oldest_seconds",
1697
+ unit: "Seconds",
1698
+ description: "Age of the oldest un-normalized source_event. The latency signal count cannot give.",
1699
+ dimensions: ["environment"],
1700
+ treatMissingData: "breaching",
1701
+ missingDataRationale: "Same publisher as the count, so the same reasoning: absence means unobserved, not fresh."
1702
+ }),
1703
+ SESSION_MINING_BACKLOG_RUNS: spec({
1704
+ name: "haansi/mining/session_backlog_runs",
1705
+ unit: "Count",
1706
+ description: "session_mining_runs not in a terminal stage, all orgs. Includes rows awaiting a provider batch.",
1707
+ dimensions: ["environment", "queue"],
1708
+ treatMissingData: "breaching",
1709
+ missingDataRationale: "Published by the reconciler, not by the mining workers it measures, so absence is a reconciler fault rather than an empty queue."
1710
+ }),
1711
+ SESSION_MINING_OLDEST_SECONDS: spec({
1712
+ name: "haansi/mining/session_backlog_oldest_seconds",
1713
+ unit: "Seconds",
1714
+ description: "Age of the oldest non-terminal session mining run, by stage queue.",
1715
+ dimensions: ["environment", "queue"],
1716
+ treatMissingData: "breaching",
1717
+ missingDataRationale: "As above \u2014 reconciler-published."
1718
+ }),
1719
+ SESSION_MINING_FAILED_RUNS: spec({
1720
+ name: "haansi/mining/session_failed_runs",
1721
+ unit: "Count",
1722
+ description: "session_mining_runs in stage 'failed', all orgs.",
1723
+ dimensions: ["environment"],
1724
+ treatMissingData: "notBreaching",
1725
+ missingDataRationale: "The ONE deliberate exception. This drives alerting, not scaling: no capacity decision reads it, so a missing value cannot cost or lose anything. Treating absence as breaching would page on the reconciler being down, which its own liveness metric already covers."
1726
+ }),
1727
+ EMBEDDING_BACKLOG_ROWS: spec({
1728
+ name: "haansi/embeddings/backlog_rows",
1729
+ unit: "Count",
1730
+ description: "Rows with a NULL embedding across the intelligence tables that carry one.",
1731
+ dimensions: ["environment", "workload"],
1732
+ treatMissingData: "breaching",
1733
+ missingDataRationale: "This is the metric that must be able to wake a fleet from zero, so it is published by the reconciler and never by an embedding worker. A worker-published version of it would read as zero precisely when the fleet is asleep with work pending \u2014 the failure this whole phase exists to remove."
1734
+ }),
1735
+ EMBEDDING_BACKLOG_OLDEST_SECONDS: spec({
1736
+ name: "haansi/embeddings/backlog_oldest_seconds",
1737
+ unit: "Seconds",
1738
+ description: "Age of the oldest row still missing an embedding.",
1739
+ dimensions: ["environment", "workload"],
1740
+ treatMissingData: "breaching",
1741
+ missingDataRationale: "As above \u2014 reconciler-published wake source."
1742
+ }),
1743
+ EMBEDDING_CLAIMS_PENDING: spec({
1744
+ name: "haansi/embeddings/claims_pending",
1745
+ unit: "Count",
1746
+ description: "embedding_claims awaiting a worker (pending, or leased with an expired lease).",
1747
+ dimensions: ["environment"],
1748
+ treatMissingData: "breaching",
1749
+ missingDataRationale: "Reconciler-published, so absence means the queue is unobserved rather than empty. This is the signal that sizes the worker fleet; reading silence as zero would scale it away with work queued."
1750
+ }),
1751
+ EMBEDDING_CLAIMS_ACTIVE_LEASES: spec({
1752
+ name: "haansi/embeddings/claims_active_leases",
1753
+ unit: "Count",
1754
+ description: "Claims currently leased with an unexpired lease \u2014 work in flight right now.",
1755
+ dimensions: ["environment"],
1756
+ treatMissingData: "breaching",
1757
+ missingDataRationale: "This is the SCALE-IN PROTECTION signal: a non-zero value means a worker is mid-inference and must not be terminated. Absence has to read as 'possibly busy', because the alternative \u2014 assuming idle \u2014 kills a worker holding a lease and the claim then waits out its full lease window before anyone retries it."
1758
+ }),
1759
+ EMBEDDING_CLAIMS_OLDEST_SECONDS: spec({
1760
+ name: "haansi/embeddings/claims_oldest_seconds",
1761
+ unit: "Seconds",
1762
+ description: "Age of the oldest outstanding claim. The latency signal a count cannot give.",
1763
+ dimensions: ["environment"],
1764
+ treatMissingData: "breaching",
1765
+ missingDataRationale: "As above. A queue of five that has not moved in an hour is a stuck consumer, and a count-only alarm reads it as healthy."
1766
+ }),
1767
+ RECONCILER_LIVENESS: spec({
1768
+ name: "haansi/reconciler/liveness",
1769
+ unit: "Count",
1770
+ description: "Emitted as 1 by every successful reconciler run. The publisher's own heartbeat.",
1771
+ dimensions: ["environment"],
1772
+ treatMissingData: "breaching",
1773
+ missingDataRationale: "Every other metric here declares that its absence means 'the reconciler is broken'. This is the metric that says so directly, so an operator gets one alarm naming the cause instead of six naming symptoms."
1774
+ })
1775
+ };
1776
+ }
1777
+ });
1778
+
1654
1779
  // ../../packages/@company/observability/src/middleware.ts
1655
1780
  var init_middleware = __esm({
1656
1781
  "../../packages/@company/observability/src/middleware.ts"() {
@@ -1666,6 +1791,15 @@ var init_health = __esm({
1666
1791
  }
1667
1792
  });
1668
1793
 
1794
+ // ../../packages/@company/observability/src/degradation.ts
1795
+ var init_degradation = __esm({
1796
+ "../../packages/@company/observability/src/degradation.ts"() {
1797
+ "use strict";
1798
+ init_types();
1799
+ init_observability_service();
1800
+ }
1801
+ });
1802
+
1669
1803
  // ../../packages/@company/observability/src/index.ts
1670
1804
  var defaultObservability, getDefaultObservability, observability;
1671
1805
  var init_src2 = __esm({
@@ -1674,8 +1808,11 @@ var init_src2 = __esm({
1674
1808
  init_observability_service();
1675
1809
  init_logger();
1676
1810
  init_metrics();
1811
+ init_scaling_metrics();
1677
1812
  init_middleware();
1678
1813
  init_health();
1814
+ init_degradation();
1815
+ init_types();
1679
1816
  defaultObservability = null;
1680
1817
  getDefaultObservability = () => {
1681
1818
  if (!defaultObservability) {
@@ -1956,10 +2093,11 @@ var init_agents = __esm({
1956
2093
  }
1957
2094
  });
1958
2095
 
1959
- // ../service-capture/claude-sessions/collector.ts
2096
+ // ../service-capture/agent-sessions/collector.ts
1960
2097
  var collector_exports = {};
1961
2098
  __export(collector_exports, {
1962
- collect: () => collect
2099
+ collect: () => collect,
2100
+ uploadSession: () => uploadSession
1963
2101
  });
1964
2102
  function edgeScrub(content) {
1965
2103
  let result = content;
@@ -2122,9 +2260,12 @@ async function getUploadedSessions(apiUrl2, token) {
2122
2260
  });
2123
2261
  }
2124
2262
  return map2;
2125
- } catch {
2126
- logger.warn("Could not fetch uploaded sessions list, will upload all");
2127
- return /* @__PURE__ */ new Map();
2263
+ } catch (err) {
2264
+ void logger.warn(
2265
+ "Could not fetch uploaded sessions list \u2014 skipping this run rather than re-uploading everything",
2266
+ { error: err?.message ?? String(err) }
2267
+ );
2268
+ return null;
2128
2269
  }
2129
2270
  }
2130
2271
  async function uploadSession(sessionId, content, apiUrl2, token, agentSource, gitInfo) {
@@ -2139,19 +2280,86 @@ async function uploadSession(sessionId, content, apiUrl2, token, agentSource, gi
2139
2280
  if (gitInfo?.gitRemoteUrl) headers["x-git-remote-url"] = gitInfo.gitRemoteUrl;
2140
2281
  if (gitInfo?.gitCommit) headers["x-git-commit"] = gitInfo.gitCommit;
2141
2282
  if (gitInfo?.gitBranch) headers["x-git-branch"] = gitInfo.gitBranch;
2142
- const response = await fetch(`${apiUrl2}/api/v1/capture/sessions/raw`, {
2143
- method: "POST",
2144
- headers,
2145
- body: new Uint8Array(body)
2146
- });
2147
- if (!response.ok) {
2283
+ for (let attempt = 0; ; attempt += 1) {
2284
+ const response = await fetch(`${apiUrl2}/api/v1/capture/sessions/raw`, {
2285
+ method: "POST",
2286
+ headers,
2287
+ body: new Uint8Array(body)
2288
+ });
2289
+ if (response.ok) return;
2290
+ if (response.status === 429 && attempt < UPLOAD_RATE_LIMIT_RETRIES) {
2291
+ const retryAfter = Number(response.headers.get("Retry-After"));
2292
+ const waitMs = Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * 1e3 : DEFAULT_RATE_LIMIT_WAIT_MS;
2293
+ await new Promise((resolve4) => setTimeout(resolve4, waitMs));
2294
+ continue;
2295
+ }
2148
2296
  const text = await response.text().catch(() => "");
2149
- throw new Error(
2297
+ const err = new Error(
2150
2298
  `Upload failed (${response.status}): ${text.slice(0, 200)}`
2151
2299
  );
2300
+ if (response.status === 429) {
2301
+ err.rateLimited = true;
2302
+ }
2303
+ throw err;
2152
2304
  }
2153
2305
  }
2154
- async function collect(options) {
2306
+ function acquireRunLock() {
2307
+ try {
2308
+ (0, import_fs3.mkdirSync)((0, import_path3.join)((0, import_os3.homedir)(), ".haansi"), { recursive: true });
2309
+ } catch {
2310
+ }
2311
+ for (let attempt = 0; attempt < 2; attempt++) {
2312
+ try {
2313
+ const fd = (0, import_fs3.openSync)(LOCK_FILE, "wx");
2314
+ (0, import_fs3.writeSync)(fd, String(process.pid));
2315
+ return fd;
2316
+ } catch {
2317
+ const holder = Number(
2318
+ (() => {
2319
+ try {
2320
+ return (0, import_fs3.readFileSync)(LOCK_FILE, "utf-8").trim();
2321
+ } catch {
2322
+ return "";
2323
+ }
2324
+ })()
2325
+ );
2326
+ if (!holder || !isProcessAlive(holder)) {
2327
+ try {
2328
+ (0, import_fs3.unlinkSync)(LOCK_FILE);
2329
+ } catch {
2330
+ }
2331
+ continue;
2332
+ }
2333
+ void logger.warn(
2334
+ "Another collector run is in progress \u2014 skipping this run",
2335
+ {
2336
+ holderPid: holder
2337
+ }
2338
+ );
2339
+ return null;
2340
+ }
2341
+ }
2342
+ return null;
2343
+ }
2344
+ function isProcessAlive(pid) {
2345
+ try {
2346
+ process.kill(pid, 0);
2347
+ return true;
2348
+ } catch (err) {
2349
+ return err?.code === "EPERM";
2350
+ }
2351
+ }
2352
+ function releaseRunLock(fd) {
2353
+ try {
2354
+ (0, import_fs3.closeSync)(fd);
2355
+ } catch {
2356
+ }
2357
+ try {
2358
+ (0, import_fs3.unlinkSync)(LOCK_FILE);
2359
+ } catch {
2360
+ }
2361
+ }
2362
+ async function collectLocked(options) {
2155
2363
  const apiUrl2 = process.env.HAANSI_API_URL ?? DEFAULT_API_URL2;
2156
2364
  const token = resolveToken2();
2157
2365
  if (!token && !options.dryRun) {
@@ -2170,7 +2378,15 @@ async function collect(options) {
2170
2378
  let uploaded = 0;
2171
2379
  let skipped = 0;
2172
2380
  let errors = 0;
2381
+ let deferred = 0;
2173
2382
  const remoteMap = options.forceAll ? /* @__PURE__ */ new Map() : await getUploadedSessions(apiUrl2, token);
2383
+ if (remoteMap === null) {
2384
+ void logger.warn("Skipping collection: server inventory unavailable", {
2385
+ localSessions: localSessions.length,
2386
+ hint: "re-authenticate with `haansi auth login` if this persists"
2387
+ });
2388
+ return { uploaded: 0, skipped: localSessions.length, errors: 0 };
2389
+ }
2174
2390
  const toUpload = [];
2175
2391
  for (const session of localSessions) {
2176
2392
  const remote = remoteMap.get(session.sessionId);
@@ -2246,11 +2462,22 @@ async function collect(options) {
2246
2462
  }
2247
2463
  );
2248
2464
  } catch (err) {
2249
- errors++;
2250
- logger.error(`Failed to upload ${shortId}`, { err: err.message });
2465
+ if (err?.rateLimited) {
2466
+ deferred++;
2467
+ void logger.info(`Rate limited, will retry next run: ${shortId}`);
2468
+ } else {
2469
+ errors++;
2470
+ logger.error(`Failed to upload ${shortId}`, { err: err.message });
2471
+ }
2251
2472
  }
2252
2473
  }
2253
- return { uploaded, skipped, errors };
2474
+ if (deferred > 0) {
2475
+ void logger.info(
2476
+ `${deferred} session(s) deferred by the upload rate limit \u2014 they remain queued`,
2477
+ { deferred, uploaded }
2478
+ );
2479
+ }
2480
+ return { uploaded, skipped, errors, deferred };
2254
2481
  }
2255
2482
  async function main() {
2256
2483
  const forceAll = process.argv.includes("--force-all");
@@ -2269,9 +2496,19 @@ async function main() {
2269
2496
  });
2270
2497
  if (errors > 0) process.exit(1);
2271
2498
  }
2272
- var import_dotenv, import_path3, import_fs3, import_os3, import_zlib, import_child_process, logger, DEFAULT_API_URL2, CREDENTIALS_FILE4, MIN_LINES, EDGE_SCRUB_PATTERNS, GIT_BIN_CANDIDATES, resolvedGitBin;
2499
+ async function collect(options) {
2500
+ if (options.dryRun) return collectLocked(options);
2501
+ const lock = acquireRunLock();
2502
+ if (lock === null) return { uploaded: 0, skipped: 0, errors: 0 };
2503
+ try {
2504
+ return await collectLocked(options);
2505
+ } finally {
2506
+ releaseRunLock(lock);
2507
+ }
2508
+ }
2509
+ var import_dotenv, import_path3, import_fs3, import_os3, import_zlib, import_child_process, logger, DEFAULT_API_URL2, CREDENTIALS_FILE4, LOCK_FILE, MIN_LINES, EDGE_SCRUB_PATTERNS, GIT_BIN_CANDIDATES, resolvedGitBin, UPLOAD_RATE_LIMIT_RETRIES, DEFAULT_RATE_LIMIT_WAIT_MS;
2273
2510
  var init_collector = __esm({
2274
- "../service-capture/claude-sessions/collector.ts"() {
2511
+ "../service-capture/agent-sessions/collector.ts"() {
2275
2512
  "use strict";
2276
2513
  import_dotenv = __toESM(require_main());
2277
2514
  import_path3 = require("path");
@@ -2286,6 +2523,7 @@ var init_collector = __esm({
2286
2523
  logger = createLogger("session-collector");
2287
2524
  DEFAULT_API_URL2 = SERVICE_URL_DEFAULTS.haansiApiBaseUrl;
2288
2525
  CREDENTIALS_FILE4 = (0, import_path3.join)((0, import_os3.homedir)(), ".haansi", "credentials.json");
2526
+ LOCK_FILE = (0, import_path3.join)((0, import_os3.homedir)(), ".haansi", "collector.lock");
2289
2527
  MIN_LINES = 4;
2290
2528
  EDGE_SCRUB_PATTERNS = [
2291
2529
  {
@@ -2332,6 +2570,8 @@ var init_collector = __esm({
2332
2570
  "/usr/local/bin/git",
2333
2571
  "/usr/bin/git"
2334
2572
  ];
2573
+ UPLOAD_RATE_LIMIT_RETRIES = 2;
2574
+ DEFAULT_RATE_LIMIT_WAIT_MS = 6e4;
2335
2575
  if (require.main === module) {
2336
2576
  main().catch((err) => {
2337
2577
  logger.error("Collector failed", err);
@@ -3358,7 +3598,50 @@ var require_node_cron = __commonJS({
3358
3598
  }
3359
3599
  });
3360
3600
 
3361
- // ../service-capture/claude-sessions/collector-daemon.ts
3601
+ // ../service-capture/agent-sessions/log-rotation.ts
3602
+ function rotateLogIfLarge(logFile, maxBytes = MAX_LOG_BYTES, keepBytes = KEEP_TAIL_BYTES) {
3603
+ let sizeBefore = 0;
3604
+ try {
3605
+ sizeBefore = (0, import_fs4.statSync)(logFile).size;
3606
+ } catch {
3607
+ return { rotated: false, sizeBefore: 0, tailKept: 0 };
3608
+ }
3609
+ if (sizeBefore <= maxBytes) {
3610
+ return { rotated: false, sizeBefore, tailKept: 0 };
3611
+ }
3612
+ let tailKept = 0;
3613
+ try {
3614
+ const start = Math.max(0, sizeBefore - keepBytes);
3615
+ const length = sizeBefore - start;
3616
+ const buf = Buffer.alloc(length);
3617
+ const fd = (0, import_fs4.openSync)(logFile, "r");
3618
+ try {
3619
+ (0, import_fs4.readSync)(fd, buf, 0, length, start);
3620
+ } finally {
3621
+ (0, import_fs4.closeSync)(fd);
3622
+ }
3623
+ (0, import_fs4.writeFileSync)(`${logFile}.1`, buf);
3624
+ tailKept = length;
3625
+ } catch {
3626
+ }
3627
+ try {
3628
+ (0, import_fs4.truncateSync)(logFile, 0);
3629
+ } catch {
3630
+ return { rotated: false, sizeBefore, tailKept };
3631
+ }
3632
+ return { rotated: true, sizeBefore, tailKept };
3633
+ }
3634
+ var import_fs4, MAX_LOG_BYTES, KEEP_TAIL_BYTES;
3635
+ var init_log_rotation = __esm({
3636
+ "../service-capture/agent-sessions/log-rotation.ts"() {
3637
+ "use strict";
3638
+ import_fs4 = require("fs");
3639
+ MAX_LOG_BYTES = 10 * 1024 * 1024;
3640
+ KEEP_TAIL_BYTES = 1 * 1024 * 1024;
3641
+ }
3642
+ });
3643
+
3644
+ // ../service-capture/agent-sessions/collector-daemon.ts
3362
3645
  var collector_daemon_exports = {};
3363
3646
  function resolveAgentsEnv() {
3364
3647
  const raw = process.env.HAANSI_AGENTS;
@@ -3369,16 +3652,27 @@ function resolveAgentsEnv() {
3369
3652
  }
3370
3653
  function getOnDiskVersion() {
3371
3654
  try {
3372
- const realEntry = (0, import_fs4.realpathSync)(process.argv[1]);
3655
+ const realEntry = (0, import_fs5.realpathSync)(process.argv[1]);
3373
3656
  const pkgPath = (0, import_path4.resolve)((0, import_path4.dirname)(realEntry), "..", "package.json");
3374
- if (!(0, import_fs4.existsSync)(pkgPath)) return null;
3375
- const pkg = JSON.parse((0, import_fs4.readFileSync)(pkgPath, "utf-8"));
3657
+ if (!(0, import_fs5.existsSync)(pkgPath)) return null;
3658
+ const pkg = JSON.parse((0, import_fs5.readFileSync)(pkgPath, "utf-8"));
3376
3659
  return pkg.version ?? null;
3377
3660
  } catch {
3378
3661
  return null;
3379
3662
  }
3380
3663
  }
3664
+ function trimLog() {
3665
+ const { rotated, sizeBefore, tailKept } = rotateLogIfLarge(LOG_FILE);
3666
+ if (rotated) {
3667
+ logger2.info("Rotated daemon log", {
3668
+ sizeBefore,
3669
+ tailKept,
3670
+ previous: `${LOG_FILE}.1`
3671
+ });
3672
+ }
3673
+ }
3381
3674
  async function run() {
3675
+ trimLog();
3382
3676
  const agents = resolveAgentsEnv();
3383
3677
  logger2.info("Collect run starting", { agents: agents?.join(",") ?? "all" });
3384
3678
  const start = Date.now();
@@ -3420,21 +3714,24 @@ async function main2() {
3420
3714
  process.on("SIGTERM", () => shutdown("SIGTERM"));
3421
3715
  process.on("SIGINT", () => shutdown("SIGINT"));
3422
3716
  }
3423
- var import_dotenv2, import_fs4, import_path4, import_node_cron, logger2, IN_MEMORY_VERSION, SCHEDULE;
3717
+ var import_dotenv2, import_fs5, import_path4, import_os4, import_node_cron, logger2, IN_MEMORY_VERSION, SCHEDULE, LOG_FILE;
3424
3718
  var init_collector_daemon = __esm({
3425
- "../service-capture/claude-sessions/collector-daemon.ts"() {
3719
+ "../service-capture/agent-sessions/collector-daemon.ts"() {
3426
3720
  "use strict";
3427
3721
  import_dotenv2 = __toESM(require_main());
3428
- import_fs4 = require("fs");
3722
+ import_fs5 = require("fs");
3429
3723
  import_path4 = require("path");
3724
+ import_os4 = require("os");
3430
3725
  import_node_cron = __toESM(require_node_cron());
3431
3726
  init_collector();
3432
3727
  init_src2();
3728
+ init_log_rotation();
3433
3729
  (0, import_dotenv2.config)({ path: (0, import_path4.resolve)(__dirname, "../../../.env") });
3434
3730
  logger2 = createLogger("collector-daemon");
3435
3731
  IN_MEMORY_VERSION = // eslint-disable-next-line @typescript-eslint/no-require-imports
3436
3732
  require_package().version;
3437
3733
  SCHEDULE = process.env.HAANSI_COLLECT_SCHEDULE ?? process.env.CLAUDE_COLLECT_SCHEDULE ?? "*/30 * * * *";
3734
+ LOG_FILE = (0, import_path4.join)((0, import_os4.homedir)(), ".haansi", "daemon.log");
3438
3735
  main2().catch((err) => {
3439
3736
  logger2.error("Daemon failed to start", err);
3440
3737
  process.exit(1);
@@ -3442,7 +3739,7 @@ var init_collector_daemon = __esm({
3442
3739
  }
3443
3740
  });
3444
3741
 
3445
- // ../service-capture/claude-sessions/http-timeout.ts
3742
+ // ../service-capture/agent-sessions/http-timeout.ts
3446
3743
  async function fetchWithTimeout(url2, init2 = {}, timeoutMs = 15e3) {
3447
3744
  const controller = new AbortController();
3448
3745
  const timer = setTimeout(() => controller.abort(), timeoutMs);
@@ -3453,7 +3750,7 @@ async function fetchWithTimeout(url2, init2 = {}, timeoutMs = 15e3) {
3453
3750
  }
3454
3751
  }
3455
3752
  var init_http_timeout = __esm({
3456
- "../service-capture/claude-sessions/http-timeout.ts"() {
3753
+ "../service-capture/agent-sessions/http-timeout.ts"() {
3457
3754
  "use strict";
3458
3755
  }
3459
3756
  });
@@ -52281,7 +52578,7 @@ var require_view = __commonJS({
52281
52578
  var dirname3 = path.dirname;
52282
52579
  var basename3 = path.basename;
52283
52580
  var extname = path.extname;
52284
- var join12 = path.join;
52581
+ var join13 = path.join;
52285
52582
  var resolve4 = path.resolve;
52286
52583
  module2.exports = View;
52287
52584
  function View(name, options) {
@@ -52343,12 +52640,12 @@ var require_view = __commonJS({
52343
52640
  };
52344
52641
  View.prototype.resolve = function resolve5(dir, file2) {
52345
52642
  var ext = this.ext;
52346
- var path2 = join12(dir, file2);
52643
+ var path2 = join13(dir, file2);
52347
52644
  var stat = tryStat(path2);
52348
52645
  if (stat && stat.isFile()) {
52349
52646
  return path2;
52350
52647
  }
52351
- path2 = join12(dir, basename3(file2, ext), "index" + ext);
52648
+ path2 = join13(dir, basename3(file2, ext), "index" + ext);
52352
52649
  stat = tryStat(path2);
52353
52650
  if (stat && stat.isFile()) {
52354
52651
  return path2;
@@ -54773,24 +55070,24 @@ var require_charset = __commonJS({
54773
55070
  function getCharsetPriority(charset, accepted, index) {
54774
55071
  var priority = { o: -1, q: 0, s: 0 };
54775
55072
  for (var i = 0; i < accepted.length; i++) {
54776
- var spec = specify(charset, accepted[i], index);
54777
- if (spec && (priority.s - spec.s || priority.q - spec.q || priority.o - spec.o) < 0) {
54778
- priority = spec;
55073
+ var spec2 = specify(charset, accepted[i], index);
55074
+ if (spec2 && (priority.s - spec2.s || priority.q - spec2.q || priority.o - spec2.o) < 0) {
55075
+ priority = spec2;
54779
55076
  }
54780
55077
  }
54781
55078
  return priority;
54782
55079
  }
54783
- function specify(charset, spec, index) {
55080
+ function specify(charset, spec2, index) {
54784
55081
  var s = 0;
54785
- if (spec.charset.toLowerCase() === charset.toLowerCase()) {
55082
+ if (spec2.charset.toLowerCase() === charset.toLowerCase()) {
54786
55083
  s |= 1;
54787
- } else if (spec.charset !== "*") {
55084
+ } else if (spec2.charset !== "*") {
54788
55085
  return null;
54789
55086
  }
54790
55087
  return {
54791
55088
  i: index,
54792
- o: spec.i,
54793
- q: spec.q,
55089
+ o: spec2.i,
55090
+ q: spec2.q,
54794
55091
  s
54795
55092
  };
54796
55093
  }
@@ -54809,11 +55106,11 @@ var require_charset = __commonJS({
54809
55106
  function compareSpecs(a, b) {
54810
55107
  return b.q - a.q || b.s - a.s || a.o - b.o || a.i - b.i || 0;
54811
55108
  }
54812
- function getFullCharset(spec) {
54813
- return spec.charset;
55109
+ function getFullCharset(spec2) {
55110
+ return spec2.charset;
54814
55111
  }
54815
- function isQuality(spec) {
54816
- return spec.q > 0;
55112
+ function isQuality(spec2) {
55113
+ return spec2.q > 0;
54817
55114
  }
54818
55115
  }
54819
55116
  });
@@ -54871,25 +55168,25 @@ var require_encoding = __commonJS({
54871
55168
  function getEncodingPriority(encoding, accepted, index) {
54872
55169
  var priority = { encoding, o: -1, q: 0, s: 0 };
54873
55170
  for (var i = 0; i < accepted.length; i++) {
54874
- var spec = specify(encoding, accepted[i], index);
54875
- if (spec && (priority.s - spec.s || priority.q - spec.q || priority.o - spec.o) < 0) {
54876
- priority = spec;
55171
+ var spec2 = specify(encoding, accepted[i], index);
55172
+ if (spec2 && (priority.s - spec2.s || priority.q - spec2.q || priority.o - spec2.o) < 0) {
55173
+ priority = spec2;
54877
55174
  }
54878
55175
  }
54879
55176
  return priority;
54880
55177
  }
54881
- function specify(encoding, spec, index) {
55178
+ function specify(encoding, spec2, index) {
54882
55179
  var s = 0;
54883
- if (spec.encoding.toLowerCase() === encoding.toLowerCase()) {
55180
+ if (spec2.encoding.toLowerCase() === encoding.toLowerCase()) {
54884
55181
  s |= 1;
54885
- } else if (spec.encoding !== "*") {
55182
+ } else if (spec2.encoding !== "*") {
54886
55183
  return null;
54887
55184
  }
54888
55185
  return {
54889
55186
  encoding,
54890
55187
  i: index,
54891
- o: spec.i,
54892
- q: spec.q,
55188
+ o: spec2.i,
55189
+ q: spec2.q,
54893
55190
  s
54894
55191
  };
54895
55192
  }
@@ -54922,11 +55219,11 @@ var require_encoding = __commonJS({
54922
55219
  function compareSpecs(a, b) {
54923
55220
  return b.q - a.q || b.s - a.s || a.o - b.o || a.i - b.i;
54924
55221
  }
54925
- function getFullEncoding(spec) {
54926
- return spec.encoding;
55222
+ function getFullEncoding(spec2) {
55223
+ return spec2.encoding;
54927
55224
  }
54928
- function isQuality(spec) {
54929
- return spec.q > 0;
55225
+ function isQuality(spec2) {
55226
+ return spec2.q > 0;
54930
55227
  }
54931
55228
  }
54932
55229
  });
@@ -54975,30 +55272,30 @@ var require_language = __commonJS({
54975
55272
  function getLanguagePriority(language, accepted, index) {
54976
55273
  var priority = { o: -1, q: 0, s: 0 };
54977
55274
  for (var i = 0; i < accepted.length; i++) {
54978
- var spec = specify(language, accepted[i], index);
54979
- if (spec && (priority.s - spec.s || priority.q - spec.q || priority.o - spec.o) < 0) {
54980
- priority = spec;
55275
+ var spec2 = specify(language, accepted[i], index);
55276
+ if (spec2 && (priority.s - spec2.s || priority.q - spec2.q || priority.o - spec2.o) < 0) {
55277
+ priority = spec2;
54981
55278
  }
54982
55279
  }
54983
55280
  return priority;
54984
55281
  }
54985
- function specify(language, spec, index) {
55282
+ function specify(language, spec2, index) {
54986
55283
  var p = parseLanguage(language);
54987
55284
  if (!p) return null;
54988
55285
  var s = 0;
54989
- if (spec.full.toLowerCase() === p.full.toLowerCase()) {
55286
+ if (spec2.full.toLowerCase() === p.full.toLowerCase()) {
54990
55287
  s |= 4;
54991
- } else if (spec.prefix.toLowerCase() === p.full.toLowerCase()) {
55288
+ } else if (spec2.prefix.toLowerCase() === p.full.toLowerCase()) {
54992
55289
  s |= 2;
54993
- } else if (spec.full.toLowerCase() === p.prefix.toLowerCase()) {
55290
+ } else if (spec2.full.toLowerCase() === p.prefix.toLowerCase()) {
54994
55291
  s |= 1;
54995
- } else if (spec.full !== "*") {
55292
+ } else if (spec2.full !== "*") {
54996
55293
  return null;
54997
55294
  }
54998
55295
  return {
54999
55296
  i: index,
55000
- o: spec.i,
55001
- q: spec.q,
55297
+ o: spec2.i,
55298
+ q: spec2.q,
55002
55299
  s
55003
55300
  };
55004
55301
  }
@@ -55017,11 +55314,11 @@ var require_language = __commonJS({
55017
55314
  function compareSpecs(a, b) {
55018
55315
  return b.q - a.q || b.s - a.s || a.o - b.o || a.i - b.i || 0;
55019
55316
  }
55020
- function getFullLanguage(spec) {
55021
- return spec.full;
55317
+ function getFullLanguage(spec2) {
55318
+ return spec2.full;
55022
55319
  }
55023
- function isQuality(spec) {
55024
- return spec.q > 0;
55320
+ function isQuality(spec2) {
55321
+ return spec2.q > 0;
55025
55322
  }
55026
55323
  }
55027
55324
  });
@@ -55076,33 +55373,33 @@ var require_mediaType = __commonJS({
55076
55373
  function getMediaTypePriority(type, accepted, index) {
55077
55374
  var priority = { o: -1, q: 0, s: 0 };
55078
55375
  for (var i = 0; i < accepted.length; i++) {
55079
- var spec = specify(type, accepted[i], index);
55080
- if (spec && (priority.s - spec.s || priority.q - spec.q || priority.o - spec.o) < 0) {
55081
- priority = spec;
55376
+ var spec2 = specify(type, accepted[i], index);
55377
+ if (spec2 && (priority.s - spec2.s || priority.q - spec2.q || priority.o - spec2.o) < 0) {
55378
+ priority = spec2;
55082
55379
  }
55083
55380
  }
55084
55381
  return priority;
55085
55382
  }
55086
- function specify(type, spec, index) {
55383
+ function specify(type, spec2, index) {
55087
55384
  var p = parseMediaType(type);
55088
55385
  var s = 0;
55089
55386
  if (!p) {
55090
55387
  return null;
55091
55388
  }
55092
- if (spec.type.toLowerCase() == p.type.toLowerCase()) {
55389
+ if (spec2.type.toLowerCase() == p.type.toLowerCase()) {
55093
55390
  s |= 4;
55094
- } else if (spec.type != "*") {
55391
+ } else if (spec2.type != "*") {
55095
55392
  return null;
55096
55393
  }
55097
- if (spec.subtype.toLowerCase() == p.subtype.toLowerCase()) {
55394
+ if (spec2.subtype.toLowerCase() == p.subtype.toLowerCase()) {
55098
55395
  s |= 2;
55099
- } else if (spec.subtype != "*") {
55396
+ } else if (spec2.subtype != "*") {
55100
55397
  return null;
55101
55398
  }
55102
- var keys = Object.keys(spec.params);
55399
+ var keys = Object.keys(spec2.params);
55103
55400
  if (keys.length > 0) {
55104
55401
  if (keys.every(function(k) {
55105
- return spec.params[k] == "*" || (spec.params[k] || "").toLowerCase() == (p.params[k] || "").toLowerCase();
55402
+ return spec2.params[k] == "*" || (spec2.params[k] || "").toLowerCase() == (p.params[k] || "").toLowerCase();
55106
55403
  })) {
55107
55404
  s |= 1;
55108
55405
  } else {
@@ -55111,8 +55408,8 @@ var require_mediaType = __commonJS({
55111
55408
  }
55112
55409
  return {
55113
55410
  i: index,
55114
- o: spec.i,
55115
- q: spec.q,
55411
+ o: spec2.i,
55412
+ q: spec2.q,
55116
55413
  s
55117
55414
  };
55118
55415
  }
@@ -55131,11 +55428,11 @@ var require_mediaType = __commonJS({
55131
55428
  function compareSpecs(a, b) {
55132
55429
  return b.q - a.q || b.s - a.s || a.o - b.o || a.i - b.i || 0;
55133
55430
  }
55134
- function getFullType(spec) {
55135
- return spec.type + "/" + spec.subtype;
55431
+ function getFullType(spec2) {
55432
+ return spec2.type + "/" + spec2.subtype;
55136
55433
  }
55137
- function isQuality(spec) {
55138
- return spec.q > 0;
55434
+ function isQuality(spec2) {
55435
+ return spec2.q > 0;
55139
55436
  }
55140
55437
  function quoteCount(string3) {
55141
55438
  var count = 0;
@@ -55993,7 +56290,7 @@ var require_send = __commonJS({
55993
56290
  var Stream = require("stream");
55994
56291
  var util2 = require("util");
55995
56292
  var extname = path.extname;
55996
- var join12 = path.join;
56293
+ var join13 = path.join;
55997
56294
  var normalize = path.normalize;
55998
56295
  var resolve4 = path.resolve;
55999
56296
  var sep = path.sep;
@@ -56165,7 +56462,7 @@ var require_send = __commonJS({
56165
56462
  return res;
56166
56463
  }
56167
56464
  parts = path2.split(sep);
56168
- path2 = normalize(join12(root, path2));
56465
+ path2 = normalize(join13(root, path2));
56169
56466
  } else {
56170
56467
  if (UP_PATH_REGEXP.test(path2)) {
56171
56468
  debug('malicious path "%s"', path2);
@@ -56298,7 +56595,7 @@ var require_send = __commonJS({
56298
56595
  if (err) return self.onStatError(err);
56299
56596
  return self.error(404);
56300
56597
  }
56301
- var p = join12(path2, self._index[i]);
56598
+ var p = join13(path2, self._index[i]);
56302
56599
  debug('stat "%s"', p);
56303
56600
  fs.stat(p, function(err2, stat) {
56304
56601
  if (err2) return next(err2);
@@ -58391,13 +58688,13 @@ var init_streamableHttp = __esm({
58391
58688
  }
58392
58689
  });
58393
58690
 
58394
- // ../service-capture/claude-sessions/mcp-server.ts
58691
+ // ../service-capture/agent-sessions/mcp-server.ts
58395
58692
  var mcp_server_exports = {};
58396
58693
  function resolveToken3() {
58397
58694
  if (process.env.HAANSI_TOKEN) return process.env.HAANSI_TOKEN;
58398
- if ((0, import_fs5.existsSync)(CREDENTIALS_FILE5)) {
58695
+ if ((0, import_fs6.existsSync)(CREDENTIALS_FILE5)) {
58399
58696
  try {
58400
- const creds = JSON.parse((0, import_fs5.readFileSync)(CREDENTIALS_FILE5, "utf-8"));
58697
+ const creds = JSON.parse((0, import_fs6.readFileSync)(CREDENTIALS_FILE5, "utf-8"));
58401
58698
  if (creds.token) return creds.token;
58402
58699
  } catch {
58403
58700
  }
@@ -58482,14 +58779,14 @@ async function main3() {
58482
58779
  }
58483
58780
  warmupInBackground();
58484
58781
  }
58485
- var import_dotenv3, import_path5, import_fs5, import_os4, DEFAULT_API_URL3, CREDENTIALS_FILE5, API_URL, AGENT_SOURCE, DEFAULT_SOURCE_TOOL, TOKEN, contextSchema, isCollecting, lastCollectTime, COLLECT_COOLDOWN_MS, mcpServer, server;
58782
+ var import_dotenv3, import_path5, import_fs6, import_os5, DEFAULT_API_URL3, CREDENTIALS_FILE5, API_URL, AGENT_SOURCE, DEFAULT_SOURCE_TOOL, TOKEN, contextSchema, isCollecting, lastCollectTime, COLLECT_COOLDOWN_MS, mcpServer, server;
58486
58783
  var init_mcp_server2 = __esm({
58487
- "../service-capture/claude-sessions/mcp-server.ts"() {
58784
+ "../service-capture/agent-sessions/mcp-server.ts"() {
58488
58785
  "use strict";
58489
58786
  import_dotenv3 = __toESM(require_main());
58490
58787
  import_path5 = require("path");
58491
- import_fs5 = require("fs");
58492
- import_os4 = require("os");
58788
+ import_fs6 = require("fs");
58789
+ import_os5 = require("os");
58493
58790
  init_collector();
58494
58791
  init_http_timeout();
58495
58792
  init_service_urls();
@@ -58499,7 +58796,7 @@ var init_mcp_server2 = __esm({
58499
58796
  init_types4();
58500
58797
  (0, import_dotenv3.config)({ path: (0, import_path5.resolve)(__dirname, "../../../.env") });
58501
58798
  DEFAULT_API_URL3 = SERVICE_URL_DEFAULTS.haansiApiBaseUrl;
58502
- CREDENTIALS_FILE5 = (0, import_path5.join)((0, import_os4.homedir)(), ".haansi", "credentials.json");
58799
+ CREDENTIALS_FILE5 = (0, import_path5.join)((0, import_os5.homedir)(), ".haansi", "credentials.json");
58503
58800
  API_URL = process.env.HAANSI_API_URL ?? DEFAULT_API_URL3;
58504
58801
  AGENT_SOURCE = process.env.HAANSI_AGENT_SOURCE?.toLowerCase();
58505
58802
  DEFAULT_SOURCE_TOOL = AGENT_SOURCE === "codex" ? "codex-mcp" : "claude-code-mcp";
@@ -58999,8 +59296,8 @@ function warnIfStaleDevBundle(haansiPath) {
58999
59296
  const bundle = (0, import_node_fs6.realpathSync)(haansiPath);
59000
59297
  const repoRoot = (0, import_node_path5.dirname)((0, import_node_path5.dirname)((0, import_node_path5.dirname)((0, import_node_path5.dirname)(bundle))));
59001
59298
  const sources = [
59002
- "apps/service-capture/claude-sessions/collector.ts",
59003
- "apps/service-capture/claude-sessions/collector-daemon.ts",
59299
+ "apps/service-capture/agent-sessions/collector.ts",
59300
+ "apps/service-capture/agent-sessions/collector-daemon.ts",
59004
59301
  "apps/service-capture/common/agents/codex.ts",
59005
59302
  "apps/service-capture/common/agents/claude-code.ts"
59006
59303
  ].map((p) => (0, import_node_path5.join)(repoRoot, p));
@@ -59041,9 +59338,9 @@ function buildPlist(haansiPath) {
59041
59338
  <key>KeepAlive</key>
59042
59339
  <true/>
59043
59340
  <key>StandardOutPath</key>
59044
- <string>${LOG_FILE}</string>
59341
+ <string>${LOG_FILE2}</string>
59045
59342
  <key>StandardErrorPath</key>
59046
- <string>${LOG_FILE}</string>
59343
+ <string>${LOG_FILE2}</string>
59047
59344
  </dict>
59048
59345
  </plist>`;
59049
59346
  }
@@ -59074,12 +59371,12 @@ async function setupDaemon(uninstall) {
59074
59371
  (0, import_node_child_process3.spawnSync)("launchctl", ["load", PLIST_PATH], { stdio: "inherit" });
59075
59372
  console.log("Daemon installed and started.");
59076
59373
  console.log(`Plist : ${PLIST_PATH}`);
59077
- console.log(`Logs : ${LOG_FILE}`);
59374
+ console.log(`Logs : ${LOG_FILE2}`);
59078
59375
  console.log(`
59079
59376
  The daemon will start automatically on every login.`);
59080
59377
  console.log(`To uninstall: haansi setup-daemon --uninstall`);
59081
59378
  }
59082
- var import_node_child_process3, import_node_fs6, import_node_path5, import_node_os5, PLIST_LABEL, PLIST_PATH, LOG_FILE;
59379
+ var import_node_child_process3, import_node_fs6, import_node_path5, import_node_os5, PLIST_LABEL, PLIST_PATH, LOG_FILE2;
59083
59380
  var init_setup_daemon = __esm({
59084
59381
  "src/commands/setup-daemon.ts"() {
59085
59382
  "use strict";
@@ -59094,7 +59391,7 @@ var init_setup_daemon = __esm({
59094
59391
  "LaunchAgents",
59095
59392
  `${PLIST_LABEL}.plist`
59096
59393
  );
59097
- LOG_FILE = (0, import_node_path5.join)((0, import_node_os5.homedir)(), ".haansi", "daemon.log");
59394
+ LOG_FILE2 = (0, import_node_path5.join)((0, import_node_os5.homedir)(), ".haansi", "daemon.log");
59098
59395
  }
59099
59396
  });
59100
59397
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "haansi",
3
- "version": "0.1.30",
3
+ "version": "0.1.32",
4
4
  "description": "Haansi CLI - Session collector and MCP server for Claude Code & OpenAI Codex",
5
5
  "bin": {
6
6
  "haansi": "./dist/haansi.js"