@standardagents/code 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -10,6 +10,69 @@ import crypto from 'crypto';
10
10
  import readline from 'readline';
11
11
 
12
12
  // src/api.ts
13
+ function classifyConnectError(err, endpoint) {
14
+ const host = endpoint.replace(/^https?:\/\//, "").replace(/\/.*$/, "");
15
+ const codes = /* @__PURE__ */ new Set();
16
+ const messages = [];
17
+ for (let e = err; e; e = e.cause) {
18
+ if (typeof e.code === "string") codes.add(e.code);
19
+ if (typeof e.message === "string") messages.push(e.message);
20
+ if (Array.isArray(e.errors)) {
21
+ for (const sub of e.errors) {
22
+ if (typeof sub?.code === "string") codes.add(sub.code);
23
+ if (typeof sub?.message === "string") messages.push(sub.message);
24
+ }
25
+ }
26
+ }
27
+ const text = messages.join(" | ");
28
+ const has = (...wanted) => wanted.some((w) => codes.has(w));
29
+ if (has("ENOTFOUND", "EAI_AGAIN")) {
30
+ return {
31
+ ok: false,
32
+ reason: `Can't resolve ${host} \u2014 no such hostname from this machine.`,
33
+ hint: "The endpoint URL is probably wrong (typo, or that server isn't running). Check the URL, your VPN, and local DNS."
34
+ };
35
+ }
36
+ if (has("ECONNREFUSED")) {
37
+ return {
38
+ ok: false,
39
+ reason: `${host} refused the connection \u2014 nothing is listening there.`,
40
+ hint: "The instance looks stopped. Start it (or check the port in the endpoint URL)."
41
+ };
42
+ }
43
+ if (has("EHOSTUNREACH", "ENETUNREACH", "ENETDOWN")) {
44
+ return {
45
+ ok: false,
46
+ reason: `No route to ${host} \u2014 the network looks down or unreachable.`,
47
+ hint: "Check your internet connection / VPN."
48
+ };
49
+ }
50
+ if (has("ETIMEDOUT", "UND_ERR_CONNECT_TIMEOUT", "UND_ERR_HEADERS_TIMEOUT") || err?.name === "TimeoutError") {
51
+ return {
52
+ ok: false,
53
+ reason: `${host} didn't answer in time.`,
54
+ hint: "The server may be overloaded or a firewall is eating the traffic. Try again."
55
+ };
56
+ }
57
+ if (has(
58
+ "UNABLE_TO_VERIFY_LEAF_SIGNATURE",
59
+ "DEPTH_ZERO_SELF_SIGNED_CERT",
60
+ "SELF_SIGNED_CERT_IN_CHAIN",
61
+ "CERT_HAS_EXPIRED",
62
+ "ERR_TLS_CERT_ALTNAME_INVALID"
63
+ ) || /certificate|TLS|SSL/i.test(text)) {
64
+ return {
65
+ ok: false,
66
+ reason: `TLS problem talking to ${host}: ${text.slice(0, 120)}`,
67
+ hint: "For local/self-signed endpoints, use the http:// URL or trust the local CA."
68
+ };
69
+ }
70
+ return {
71
+ ok: false,
72
+ reason: `Couldn't reach ${host}${text ? ` (${text.slice(0, 120)})` : ""}.`,
73
+ hint: "This is a connection problem, not a token problem \u2014 check the endpoint URL and your network."
74
+ };
75
+ }
13
76
  var ApiClient = class {
14
77
  constructor(endpoint, token) {
15
78
  this.endpoint = endpoint;
@@ -47,12 +110,54 @@ var ApiClient = class {
47
110
  }
48
111
  }
49
112
  async verify() {
113
+ return (await this.verifyDetailed()).ok;
114
+ }
115
+ /**
116
+ * Check the endpoint + token and say PRECISELY what's wrong when they
117
+ * fail. "That token didn't work" is a lie when the real problem is a typo'd
118
+ * hostname, a stopped server, or a dead network — sign-in errors must let
119
+ * the user tell those apart from an actually-rejected key.
120
+ */
121
+ async verifyDetailed() {
122
+ let res;
50
123
  try {
51
- await this.json("/api/auth/me");
52
- return true;
53
- } catch {
54
- return false;
124
+ res = await fetch(`${this.endpoint}/api/auth/me`, {
125
+ headers: { Authorization: `Bearer ${this.token}` },
126
+ signal: AbortSignal.timeout(15e3)
127
+ });
128
+ } catch (err) {
129
+ return classifyConnectError(err, this.endpoint);
130
+ }
131
+ if (res.ok) return { ok: true };
132
+ if (res.status === 401) {
133
+ return {
134
+ ok: false,
135
+ reason: "The instance rejected this token \u2014 it's invalid, expired, or was revoked.",
136
+ hint: "Create a fresh API key in the instance settings (or press Enter to sign in with your browser). Keys only work on the instance that minted them."
137
+ };
55
138
  }
139
+ if (res.status === 403) {
140
+ return {
141
+ ok: false,
142
+ reason: "The token was recognized, but this account isn't allowed to use the API here.",
143
+ hint: "Ask an instance admin to grant access, or use a different account's key."
144
+ };
145
+ }
146
+ if (res.status === 404 || res.status === 405) {
147
+ return {
148
+ ok: false,
149
+ reason: `${this.endpoint} answered, but it doesn't look like a Standard Agents instance (no /api/auth/me).`,
150
+ hint: "Double-check the endpoint URL \u2014 this may be a different service."
151
+ };
152
+ }
153
+ if (res.status >= 500) {
154
+ return {
155
+ ok: false,
156
+ reason: `The instance hit an error while checking the token (HTTP ${res.status}).`,
157
+ hint: "That's a server-side problem \u2014 check the instance's logs and try again."
158
+ };
159
+ }
160
+ return { ok: false, reason: `The instance answered HTTP ${res.status} while checking the token.` };
56
161
  }
57
162
  /** Agent name → display title map (best effort; for labeling subagents). */
58
163
  async listAgents() {
@@ -69,12 +174,22 @@ var ApiClient = class {
69
174
  if (!id) throw new Error("Thread create returned no id");
70
175
  return id;
71
176
  }
72
- /** List threads for an agent, optionally filtering to those carrying all given tags. */
177
+ /**
178
+ * List threads for an agent (or several variants of it), optionally
179
+ * filtering to those carrying all given tags. Multiple ids exist because
180
+ * the instance's model gearbox hands a session between agent variants — a
181
+ * thread that last ran in another gear must still show up for resume.
182
+ */
73
183
  async listThreads(agentId, requireTags) {
74
- const res = await this.json(
75
- `/api/threads?agent_id=${encodeURIComponent(agentId)}&limit=100`
184
+ const ids = Array.isArray(agentId) ? agentId : [agentId];
185
+ const pages = await Promise.all(
186
+ ids.map(
187
+ (id) => this.json(
188
+ `/api/threads?agent_id=${encodeURIComponent(id)}&limit=100`
189
+ ).catch(() => [])
190
+ )
76
191
  );
77
- const arr = Array.isArray(res) ? res : res.threads || [];
192
+ const arr = pages.flatMap((res) => Array.isArray(res) ? res : res.threads || []);
78
193
  return arr.map((t) => ({
79
194
  id: t.id,
80
195
  tags: Array.isArray(t.tags) ? t.tags : [],
@@ -384,9 +499,6 @@ function newFileLines(content) {
384
499
  return out;
385
500
  }
386
501
  function highlightBash(cmd) {
387
- const CYAN2 = "\x1B[36m";
388
- const GREEN = "\x1B[32m";
389
- const MAGENTA = "\x1B[35m";
390
502
  let out = "";
391
503
  let i = 0;
392
504
  let expectProgram = true;
@@ -398,7 +510,7 @@ function highlightBash(cmd) {
398
510
  if (ch === '"' && cmd[j] === "\\") j++;
399
511
  j++;
400
512
  }
401
- out += `${GREEN}${cmd.slice(i, Math.min(j + 1, cmd.length))}${RESET}`;
513
+ out += `${DIM}${cmd.slice(i, Math.min(j + 1, cmd.length))}${RESET}`;
402
514
  i = j + 1;
403
515
  continue;
404
516
  }
@@ -408,7 +520,7 @@ function highlightBash(cmd) {
408
520
  }
409
521
  const op = cmd.slice(i).match(/^(\|\||&&|\||;|>>|>|<)/);
410
522
  if (op) {
411
- out += `${MAGENTA}${op[1]}${RESET}`;
523
+ out += `${DIM}${op[1]}${RESET}`;
412
524
  i += op[1].length;
413
525
  expectProgram = true;
414
526
  continue;
@@ -416,19 +528,24 @@ function highlightBash(cmd) {
416
528
  const word = cmd.slice(i).match(/^[^\s'"#|;&<>]+/);
417
529
  if (word) {
418
530
  const w = word[0];
419
- if (w.startsWith("-")) out += `${DIM}${w}${RESET}`;
420
- else if (expectProgram) {
421
- out += `${CYAN2}${w}${RESET}`;
531
+ if (expectProgram && !w.startsWith("-")) {
532
+ out += w;
422
533
  expectProgram = false;
423
- } else out += w;
534
+ } else {
535
+ out += `${DIM}${w}${RESET}`;
536
+ }
424
537
  i += w.length;
425
538
  continue;
426
539
  }
427
- out += ch;
540
+ out += `${DIM}${ch}${RESET}`;
428
541
  i++;
429
542
  }
430
543
  return out;
431
544
  }
545
+ function oneLineCommand(cmd, max) {
546
+ const collapsed = cmd.replace(/\s*\n+\s*/g, " \u23CE ").replace(/[ \t]{2,}/g, " ").trim();
547
+ return clamp(collapsed, max);
548
+ }
432
549
  var GRAD_STOPS = [
433
550
  [255, 179, 92],
434
551
  // amber
@@ -478,10 +595,6 @@ function gradientText(text, phase = 0) {
478
595
  }
479
596
  return out + "\x1B[0m";
480
597
  }
481
- function gradientArt(lines) {
482
- const n = Math.max(1, lines.length - 1);
483
- return lines.map((line, i) => gradientText(line, i / n * 0.55));
484
- }
485
598
 
486
599
  // src/bridge.ts
487
600
  var PATH_ARG_TOOLS = /* @__PURE__ */ new Set(["read_file", "grep", "glob", "write_file", "edit_file"]);
@@ -668,7 +781,7 @@ var Bridge = class {
668
781
  this.hooks.onStatus?.(callKey, null);
669
782
  }
670
783
  if (result.ok) {
671
- const display = req.tool === "bash" ? `bash: ${highlightBash(String(req.args.command ?? "").slice(0, 200))}` : summary;
784
+ const display = req.tool === "bash" ? `bash: ${highlightBash(oneLineCommand(String(req.args.command ?? ""), Math.max(40, (process.stdout.columns || 80) - 20)))}` : summary;
672
785
  let detail;
673
786
  if (req.tool === "edit_file") {
674
787
  detail = diffLines(String(req.args.old_string ?? ""), String(req.args.new_string ?? ""));
@@ -717,7 +830,7 @@ function describe(req) {
717
830
  case "run_skill_script":
718
831
  return `skill ${a.skill}: run ${a.entry}`;
719
832
  case "bash":
720
- return `bash: ${String(a.command).slice(0, 80)}`;
833
+ return `bash: ${oneLineCommand(String(a.command ?? ""), 80)}`;
721
834
  default:
722
835
  return `${req.tool} ${JSON.stringify(a).slice(0, 80)}`;
723
836
  }
@@ -1199,6 +1312,9 @@ ${truncated}`
1199
1312
  const command = String(args.command || "");
1200
1313
  if (!command.trim()) return { ok: false, error: "command is required" };
1201
1314
  const cwd = args.cwd ? this.resolve(String(args.cwd)) : this.projectDir;
1315
+ if (!fs4.existsSync(cwd)) {
1316
+ return { ok: false, error: `cwd does not exist: ${cwd}` };
1317
+ }
1202
1318
  const id = crypto.randomUUID().slice(0, 8);
1203
1319
  const logPath = path3.join(LOG_DIR, `${id}.log`);
1204
1320
  let out;
@@ -1215,15 +1331,22 @@ ${truncated}`
1215
1331
  fs4.closeSync(out);
1216
1332
  return { ok: false, error: `Failed to start: ${err instanceof Error ? err.message : String(err)}` };
1217
1333
  }
1334
+ let spawnError = null;
1335
+ child.on("error", (err) => {
1336
+ spawnError = err;
1337
+ });
1218
1338
  fs4.closeSync(out);
1219
1339
  const pid = child.pid;
1220
- if (!pid) return { ok: false, error: "Process failed to start (no pid)." };
1221
1340
  let earlyExit;
1222
1341
  const onEarlyExit = (code) => {
1223
1342
  earlyExit = code;
1224
1343
  };
1225
1344
  child.on("exit", onEarlyExit);
1226
1345
  await new Promise((r) => setTimeout(r, STARTUP_GRACE_MS));
1346
+ if (spawnError) {
1347
+ return { ok: false, error: `Failed to start: ${spawnError.message}` };
1348
+ }
1349
+ if (!pid) return { ok: false, error: "Process failed to start (no pid)." };
1227
1350
  if (earlyExit !== void 0 || !isAlive2(pid)) {
1228
1351
  const tail = await readLogTail(logPath, 15);
1229
1352
  const code = earlyExit ?? "unknown";
@@ -2350,24 +2473,22 @@ var Tui = class _Tui {
2350
2473
  spinnerFrame() {
2351
2474
  return `${C.bold}${C.cyan}${FRAMES[Math.floor(Date.now() / 100) % FRAMES.length]}${C.reset}`;
2352
2475
  }
2353
- /**
2354
- * "↑X ↓Y" cumulative token totals (greyed — low-priority), plus a context
2355
- * window gauge "ctx N%" when known. The gauge colour ramps with fill (green →
2356
- * yellow → red) so the user can see compaction approaching at a glance.
2357
- */
2476
+ /** "↑X ↓Y" cumulative token totals (greyed — low-priority). */
2358
2477
  tokensText() {
2359
2478
  const parts = [];
2360
2479
  if (this.tokensIn > 0) parts.push(`\u2191${this.fmtTokens(this.tokensIn)}`);
2361
2480
  if (this.tokensOut > 0) parts.push(`\u2193${this.fmtTokens(this.tokensOut)}`);
2362
- let out = parts.length ? `${C.gray}${parts.join(" ")}${C.reset}` : "";
2363
- if (this.contextPct != null) {
2364
- const pct = this.contextPct;
2365
- const calmGreen = "\x1B[38;5;65m";
2366
- const col = pct >= 85 ? C.red : pct >= 70 ? C.yellow : calmGreen;
2367
- const gauge = `${col}ctx ${pct}%${C.reset}`;
2368
- out = out ? `${out} ${gauge}` : gauge;
2369
- }
2370
- return out;
2481
+ return parts.length ? `${C.gray}${parts.join(" ")}${C.reset}` : "";
2482
+ }
2483
+ /**
2484
+ * Understated context gauge, right-aligned on the summary line: just "N%",
2485
+ * dim, and INVISIBLE until the conversation is more than 85% of the way to
2486
+ * the compaction point (the percentage is scaled so 100% = where background
2487
+ * compaction triggers, not the raw model window). Most sessions never see it.
2488
+ */
2489
+ contextGaugeText() {
2490
+ if (this.contextPct == null || this.contextPct <= 85) return "";
2491
+ return `${C.dim}${C.gray}${this.contextPct}%${C.reset}`;
2371
2492
  }
2372
2493
  /**
2373
2494
  * Set the context-window fill percentage (0–100), or null to hide it.
@@ -2380,9 +2501,9 @@ var Tui = class _Tui {
2380
2501
  this.contextPct = next;
2381
2502
  this.renderBottom();
2382
2503
  }
2383
- /** Plain "ctx N%" label (no ANSI) for menu hints, or "" when unknown. */
2504
+ /** Plain context label (no ANSI) for menu hints, or "" when unknown. */
2384
2505
  contextPctLabel() {
2385
- return this.contextPct == null ? "" : `ctx ${this.contextPct}%`;
2506
+ return this.contextPct == null ? "" : `context ${this.contextPct}%`;
2386
2507
  }
2387
2508
  /** Register the slash commands shown in the inline `/` palette. */
2388
2509
  setCommands(commands) {
@@ -2417,24 +2538,34 @@ var Tui = class _Tui {
2417
2538
  */
2418
2539
  statusLineText(cols2) {
2419
2540
  const tk = this.tokensText();
2541
+ const gauge = this.contextGaugeText();
2542
+ const gaugeReserve = gauge ? this.visibleWidth(gauge) + 1 : 0;
2543
+ let line;
2420
2544
  if (this.working) {
2421
2545
  const el = this.formatElapsed(Date.now() - this.workingStart);
2422
2546
  const right = `${C.dim}${el}${C.reset}${tk ? " " + tk : ""}`;
2423
2547
  const head = `${this.spinnerFrame()} ${C.bold}Working${C.reset}`;
2424
- const avail = Math.max(0, cols2 - this.visibleWidth(head) - this.visibleWidth(right) - 2);
2548
+ const avail = Math.max(
2549
+ 0,
2550
+ cols2 - this.visibleWidth(head) - this.visibleWidth(right) - 2 - gaugeReserve
2551
+ );
2425
2552
  let stepPart = "";
2426
2553
  if (this.step && avail > 1) {
2427
2554
  let s = this.step;
2428
2555
  if (s.length > avail) s = s.slice(0, avail - 1) + "\u2026";
2429
2556
  stepPart = ` ${C.dim}${s}${C.reset}`;
2430
2557
  }
2431
- return `${head}${stepPart} ${right}`;
2432
- }
2433
- if (this.goalComplete) {
2558
+ line = `${head}${stepPart} ${right}`;
2559
+ } else if (this.goalComplete) {
2434
2560
  const done = `${C.bold}${gradientText("\u2713 Goal complete.")}${C.reset}`;
2435
- return tk ? `${done} ${tk}` : done;
2561
+ line = tk ? `${done} ${tk}` : done;
2562
+ } else {
2563
+ line = tk ? tk : null;
2436
2564
  }
2437
- return tk ? tk : null;
2565
+ if (!gauge) return line;
2566
+ const base = line ?? "";
2567
+ const pad = Math.max(1, cols2 - 1 - this.visibleWidth(base) - this.visibleWidth(gauge));
2568
+ return `${base}${" ".repeat(pad)}${gauge}`;
2438
2569
  }
2439
2570
  /** The prompt line prefix (with ANSI colour) that precedes the typed text. */
2440
2571
  promptPrefix() {
@@ -3724,6 +3855,7 @@ function saveDefaultEndpoint(endpoint) {
3724
3855
 
3725
3856
  // src/index.ts
3726
3857
  var AGENT_ID = "standard_code_agent";
3858
+ var AGENT_ID_VARIANTS = [AGENT_ID, "standard_code_high_agent"];
3727
3859
  var c = {
3728
3860
  reset: "\x1B[0m",
3729
3861
  dim: "\x1B[2m",
@@ -3874,11 +4006,10 @@ function printWelcome(endpoint, projectDir) {
3874
4006
  `${c.dim}${dir}${c.reset}`
3875
4007
  ];
3876
4008
  const markWidth = Math.max(...LOGO_MARK.map((l) => [...l].length));
3877
- const gradMark = gradientArt(LOGO_MARK);
3878
4009
  const metaTop = Math.floor((LOGO_MARK.length - meta.length) / 2);
3879
4010
  stdout.write("\n");
3880
4011
  for (let i = 0; i < LOGO_MARK.length; i++) {
3881
- const glyph = gradMark[i].padEnd(markWidth + (gradMark[i].length - LOGO_MARK[i].length));
4012
+ const glyph = LOGO_MARK[i].padEnd(markWidth);
3882
4013
  const line = meta[i - metaTop];
3883
4014
  stdout.write(`${pad}${glyph}${line ? ` ${line}` : ""}
3884
4015
  `);
@@ -3987,8 +4118,22 @@ ${c.dim}Press Control-C again to exit${c.reset}
3987
4118
  }
3988
4119
  const stored = getCredential(endpoint);
3989
4120
  let api = stored ? new ApiClient(endpoint, stored.access_token) : null;
3990
- if (!api || !await api.verify()) {
4121
+ const storedCheck = api ? await api.verifyDetailed() : null;
4122
+ if (!api || !storedCheck?.ok) {
3991
4123
  const host = endpoint.replace(/^https?:\/\//, "").replace(/\/$/, "");
4124
+ if (storedCheck && !storedCheck.ok) {
4125
+ stdout.write(`${c.red}\u2717${c.reset} ${c.dim}Saved sign-in for this endpoint failed:${c.reset} ${storedCheck.reason}
4126
+ `);
4127
+ if (storedCheck.hint) stdout.write(` ${c.dim}${storedCheck.hint}${c.reset}
4128
+ `);
4129
+ stdout.write("\n");
4130
+ }
4131
+ const explainFailure = (result, prefix) => {
4132
+ stdout.write(`${c.red}\u2717${c.reset} ${prefix}${result.reason}
4133
+ `);
4134
+ if (result.hint) stdout.write(` ${c.dim}${result.hint}${c.reset}
4135
+ `);
4136
+ };
3992
4137
  stdout.write(
3993
4138
  `${c.bold}${c.white}Sign in${c.reset} ${c.dim}\u2014 connect to${c.reset} ${c.teal}${host}${c.reset}
3994
4139
  `
@@ -4008,7 +4153,8 @@ ${c.dim}Press Control-C again to exit${c.reset}
4008
4153
  });
4009
4154
  if (!got) continue;
4010
4155
  api = new ApiClient(endpoint, got);
4011
- if (await api.verify()) {
4156
+ const check2 = await api.verifyDetailed();
4157
+ if (check2.ok) {
4012
4158
  saveCredential(
4013
4159
  { endpoint, access_token: got, token_type: "Bearer", saved_at: Date.now() },
4014
4160
  { updateDefault: !endpointOverride }
@@ -4017,12 +4163,12 @@ ${c.dim}Press Control-C again to exit${c.reset}
4017
4163
  `);
4018
4164
  break;
4019
4165
  }
4020
- stdout.write(`${c.red}\u2717${c.reset} ${c.dim}Browser sign-in didn't verify. Try again.${c.reset}
4021
- `);
4166
+ explainFailure(check2, "Browser sign-in didn't verify: ");
4022
4167
  continue;
4023
4168
  }
4024
4169
  api = new ApiClient(endpoint, token);
4025
- if (await api.verify()) {
4170
+ const check = await api.verifyDetailed();
4171
+ if (check.ok) {
4026
4172
  saveCredential(
4027
4173
  { endpoint, access_token: token, token_type: "Bearer", saved_at: Date.now() },
4028
4174
  { updateDefault: !endpointOverride }
@@ -4031,8 +4177,7 @@ ${c.dim}Press Control-C again to exit${c.reset}
4031
4177
  `);
4032
4178
  break;
4033
4179
  }
4034
- stdout.write(`${c.red}\u2717${c.reset} ${c.dim}That token didn't work. Try again.${c.reset}
4035
- `);
4180
+ explainFailure(check, "");
4036
4181
  }
4037
4182
  } else if (endpointPrompted) {
4038
4183
  saveDefaultEndpoint(endpoint);
@@ -4043,7 +4188,7 @@ ${c.dim}Press Control-C again to exit${c.reset}
4043
4188
  const tags = [`path:${projectDir}`, `machine:${machine}`];
4044
4189
  let existing = [];
4045
4190
  try {
4046
- existing = await api.listThreads(AGENT_ID, tags);
4191
+ existing = await api.listThreads(AGENT_ID_VARIANTS, tags);
4047
4192
  } catch {
4048
4193
  existing = [];
4049
4194
  }
@@ -4298,9 +4443,11 @@ ${c.bold}why: ${req.requestPermission}${c.reset}` : ""}`,
4298
4443
  const subs = await api.listSubagents(threadId);
4299
4444
  activeSubagents.clear();
4300
4445
  for (const s of subs) {
4301
- if (s.status !== "running") continue;
4446
+ const status = (s.status || "").trim();
4447
+ if (status === "idle" || status === "terminated") continue;
4448
+ const detail = status && status !== "running" ? ` \u2014 ${status.slice(0, 80)}` : "";
4302
4449
  activeSubagents.set(s.id, {
4303
- label: subagentLabel(s, agentTitles),
4450
+ label: `${subagentLabel(s, agentTitles)}${detail}`,
4304
4451
  agentName: s.agent_name ?? void 0
4305
4452
  });
4306
4453
  }
@@ -4336,6 +4483,10 @@ ${c.bold}why: ${req.requestPermission}${c.reset}` : ""}`,
4336
4483
  if (activeSubagents.has(id)) scheduleReconcile();
4337
4484
  }
4338
4485
  });
4486
+ const heartbeatPoll = setInterval(() => {
4487
+ if (activeSubagents.size > 0) scheduleReconcile();
4488
+ }, 5e3);
4489
+ heartbeatPoll.unref();
4339
4490
  const quit = async () => {
4340
4491
  tui.end();
4341
4492
  const stopped = api.stop(threadId).catch(() => {
@@ -4616,10 +4767,11 @@ ${c.bold}why: ${req.requestPermission}${c.reset}` : ""}`,
4616
4767
  refreshStatus();
4617
4768
  } catch {
4618
4769
  }
4770
+ const COMPACTION_TRIGGER_FRACTION = 0.5;
4619
4771
  try {
4620
4772
  const cu = await api.kvGet(threadId, "context_usage");
4621
4773
  const used = Number(cu?.inputTokens) || 0;
4622
- const max = Number(cu?.maxContextTokens) || 0;
4774
+ const max = (Number(cu?.maxContextTokens) || 0) * COMPACTION_TRIGGER_FRACTION;
4623
4775
  tui.setContextPct(max > 0 && used > 0 ? used / max * 100 : null);
4624
4776
  } catch {
4625
4777
  }