agent-lookout 0.2.8 → 0.2.10

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.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  sortSessions
3
- } from "./chunk-JSBQMY3Q.js";
3
+ } from "./chunk-HHZWG5HI.js";
4
4
  import {
5
5
  ANSWER_DECISIONS,
6
6
  CAPABILITIES,
@@ -34,6 +34,7 @@ import {
34
34
  isRemoteSource,
35
35
  isStatusEvent,
36
36
  loopbackAddress,
37
+ messageText,
37
38
  needsYou,
38
39
  noticeTitle,
39
40
  oneLine,
@@ -48,7 +49,7 @@ import {
48
49
  waitingPhrase,
49
50
  waitingText,
50
51
  withoutWaitingText
51
- } from "./chunk-I6Q35DHC.js";
52
+ } from "./chunk-ME7TBH3B.js";
52
53
 
53
54
  // src/collector/hosts/server.ts
54
55
  import { createServer } from "node:http";
@@ -132,14 +133,14 @@ function sessionChanges(memory, snapshot) {
132
133
  }
133
134
  const found = [];
134
135
  for (const session of snapshot.sessions) {
135
- const answer = answers2.get(session.source);
136
- if (!answer || answer.sessions.has(session.id)) continue;
137
- answer.sessions.set(session.id, withoutWaitingText(session));
136
+ const answer2 = answers2.get(session.source);
137
+ if (!answer2 || answer2.sessions.has(session.id)) continue;
138
+ answer2.sessions.set(session.id, withoutWaitingText(session));
138
139
  if (startedWaiting.has(session)) {
139
140
  found.push({ event: "needs-you", session });
140
141
  continue;
141
142
  }
142
- const before = memory.seen.get(session.source)?.get(answer.basis)?.get(session.id);
143
+ const before = memory.seen.get(session.source)?.get(answer2.basis)?.get(session.id);
143
144
  if (before && before.status !== session.status && isOverStatus(session.status)) {
144
145
  found.push({ event: session.status, session });
145
146
  }
@@ -186,6 +187,7 @@ function sessionChanges(memory, snapshot) {
186
187
 
187
188
  // src/core/api.ts
188
189
  var CLEAR_HISTORY_ACTION = "clear-history";
190
+ var LAST_MESSAGE_PATH = "/api/sessions/last-message";
189
191
  var TERMINAL_JUMP_TIMEOUT_MS = 6e4;
190
192
  var JUMP_INTERVAL_MS = 1e3;
191
193
  var STOP_ACTION = "stop";
@@ -2067,6 +2069,13 @@ function send(res, status, body, headers = {}) {
2067
2069
  function fail(res, status, error, headers = {}) {
2068
2070
  send(res, status, { error }, headers);
2069
2071
  }
2072
+ function sendWhenReady(res, answer2) {
2073
+ answer2.then((ready) => send(res, ready.status, ready.body, ready.headers)).catch(() => {
2074
+ if (res.headersSent) res.end();
2075
+ else fail(res, 500, "The collector ran into an unexpected problem.");
2076
+ }).catch(() => {
2077
+ });
2078
+ }
2070
2079
  function notificationsSaid(req) {
2071
2080
  const value = req.headers[NOTIFICATIONS_HEADER.toLowerCase()];
2072
2081
  if (typeof value !== "string") return null;
@@ -2105,11 +2114,7 @@ function createApiHandler(options) {
2105
2114
  }
2106
2115
  const action = url === null ? void 0 : actions.get(url.pathname);
2107
2116
  if (action) {
2108
- action(req).then((answer2) => send(res, answer2.status, answer2.body, answer2.headers)).catch(() => {
2109
- if (res.headersSent) res.end();
2110
- else fail(res, 500, "The collector ran into an unexpected problem.");
2111
- }).catch(() => {
2112
- });
2117
+ sendWhenReady(res, action(req));
2113
2118
  return;
2114
2119
  }
2115
2120
  if (req.method !== "GET") {
@@ -2131,6 +2136,14 @@ function createApiHandler(options) {
2131
2136
  send(res, 200, serveSnapshot(poller.getSnapshot()));
2132
2137
  return;
2133
2138
  }
2139
+ case LAST_MESSAGE_PATH: {
2140
+ if (options.lastMessage) {
2141
+ sendWhenReady(res, options.lastMessage(req, url));
2142
+ return;
2143
+ }
2144
+ fail(res, 404, "There is nothing at that address.");
2145
+ return;
2146
+ }
2134
2147
  case "/api/events": {
2135
2148
  const since = numberParam(url.searchParams.get("since"));
2136
2149
  if (since === "invalid") {
@@ -2216,7 +2229,7 @@ function createApiHandler(options) {
2216
2229
  }
2217
2230
  }
2218
2231
  }
2219
- function answer(req, res) {
2232
+ function answer2(req, res) {
2220
2233
  try {
2221
2234
  route(req, res);
2222
2235
  } catch {
@@ -2227,12 +2240,12 @@ function createApiHandler(options) {
2227
2240
  return (req, res) => {
2228
2241
  const reading = options.ready?.() ?? null;
2229
2242
  if (reading === null) {
2230
- answer(req, res);
2243
+ answer2(req, res);
2231
2244
  return;
2232
2245
  }
2233
2246
  void reading.then(
2234
- () => answer(req, res),
2235
- () => answer(req, res)
2247
+ () => answer2(req, res),
2248
+ () => answer2(req, res)
2236
2249
  );
2237
2250
  };
2238
2251
  }
@@ -2451,7 +2464,7 @@ function urlFor(address) {
2451
2464
 
2452
2465
  // src/collector/hosts/standalone.ts
2453
2466
  import { existsSync } from "node:fs";
2454
- import path25 from "node:path";
2467
+ import path26 from "node:path";
2455
2468
 
2456
2469
  // src/core/mapping/claudeCodeMapping.ts
2457
2470
  var LIVE_STATUS = /* @__PURE__ */ new Map([
@@ -2838,6 +2851,16 @@ import os2 from "node:os";
2838
2851
  // src/collector/adapters/claude-code/feed.ts
2839
2852
  import { spawn } from "node:child_process";
2840
2853
 
2854
+ // src/collector/processes/childEnvironment.ts
2855
+ var OWN_SETTING = /^AGENT_LOOKOUT_/i;
2856
+ function childEnvironment(env) {
2857
+ const given = {};
2858
+ for (const [name, value] of Object.entries(env)) {
2859
+ if (!OWN_SETTING.test(name)) given[name] = value;
2860
+ }
2861
+ return given;
2862
+ }
2863
+
2841
2864
  // src/collector/processes/pids.ts
2842
2865
  function validPid(value) {
2843
2866
  return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : void 0;
@@ -2954,7 +2977,7 @@ var runProgram = (file, args, options) => new Promise((resolve) => {
2954
2977
  }, options.timeoutMs);
2955
2978
  try {
2956
2979
  child = spawn(file, [...args], {
2957
- env: options.env,
2980
+ env: childEnvironment(options.env),
2958
2981
  shell: false,
2959
2982
  // stdin is the null device, so a program that reads it gets end-of-file.
2960
2983
  // stderr is not wanted and goes the same way.
@@ -3261,6 +3284,12 @@ function createStopTargets() {
3261
3284
  };
3262
3285
  }
3263
3286
 
3287
+ // src/collector/messages/lastMessageSettings.ts
3288
+ var LAST_MESSAGE_ENV = "AGENT_LOOKOUT_LAST_MESSAGE";
3289
+ function lastMessageOff(env) {
3290
+ return env[LAST_MESSAGE_ENV]?.trim().toLowerCase() === "off";
3291
+ }
3292
+
3264
3293
  // src/collector/processes/ps.ts
3265
3294
  import { execFile } from "node:child_process";
3266
3295
  var PS_TIMEOUT_MS = 2e3;
@@ -3334,8 +3363,8 @@ function parseProcessStarts(stdout) {
3334
3363
  }
3335
3364
  var readProcessStartsWithPs = async (pids) => {
3336
3365
  if (pids.length === 0) return /* @__PURE__ */ new Map();
3337
- const answer = await runPs(psStartArgs(pids), { utc: true });
3338
- return parseProcessStarts(answer.stdout);
3366
+ const answer2 = await runPs(psStartArgs(pids), { utc: true });
3367
+ return parseProcessStarts(answer2.stdout);
3339
3368
  };
3340
3369
  function createProcessStartCheck(read = readProcessStartsWithPs, now = Date.now, ttlMs = START_CHECK_TTL_MS) {
3341
3370
  const remembered = /* @__PURE__ */ new Map();
@@ -3676,13 +3705,13 @@ function uniqueById(sessions) {
3676
3705
  }
3677
3706
 
3678
3707
  // src/collector/adapters/claude-code/stopOffers.ts
3679
- function jobIdOf(entry, answer) {
3680
- const listed = answer.find(
3708
+ function jobIdOf(entry, answer2) {
3709
+ const listed = answer2.find(
3681
3710
  (candidate) => candidate.id !== void 0 && (candidate.sessionId === entry.sessionId || candidate.sessionId === void 0 && candidate.pid === entry.pid)
3682
3711
  );
3683
3712
  return listed?.id !== void 0 && JOB_ID.test(listed.id) ? listed.id : void 0;
3684
3713
  }
3685
- function targetFor(entry, answer, options) {
3714
+ function targetFor(entry, answer2, options) {
3686
3715
  if (entry.sessionId === void 0 || entry.procStart === void 0) return null;
3687
3716
  const surface = mapClaudeCodeSurface(entry.entrypoint);
3688
3717
  const found = {
@@ -3695,12 +3724,12 @@ function targetFor(entry, answer, options) {
3695
3724
  return surface === "terminal" || surface === "vscode" ? { ...found, how: "signal" } : null;
3696
3725
  }
3697
3726
  if (entry.kind === "bg" && options.background && surface !== "desktop") {
3698
- const jobId = answer === null ? void 0 : jobIdOf(entry, answer);
3727
+ const jobId = answer2 === null ? void 0 : jobIdOf(entry, answer2);
3699
3728
  return jobId === void 0 ? null : { ...found, how: "background", jobId };
3700
3729
  }
3701
3730
  return null;
3702
3731
  }
3703
- function withStopOffers(sessions, live2, answer, options) {
3732
+ function withStopOffers(sessions, live2, answer2, options) {
3704
3733
  const byId = /* @__PURE__ */ new Map();
3705
3734
  for (const entry of live2) {
3706
3735
  if (entry.sessionId !== void 0) byId.set(`${SOURCE_ID}:${entry.sessionId}`, entry);
@@ -3711,7 +3740,7 @@ function withStopOffers(sessions, live2, answer, options) {
3711
3740
  if (session.status === "finished" || session.status === "failed") return session;
3712
3741
  const entry = byId.get(session.id);
3713
3742
  if (entry === void 0 || entry.pid !== session.pid) return session;
3714
- const target = targetFor(entry, answer, options);
3743
+ const target = targetFor(entry, answer2, options);
3715
3744
  if (target === null) return session;
3716
3745
  targets.set(session.id, target);
3717
3746
  return { ...session, stop: { how: target.how } };
@@ -3719,8 +3748,8 @@ function withStopOffers(sessions, live2, answer, options) {
3719
3748
  return { sessions: offered, targets };
3720
3749
  }
3721
3750
 
3722
- // src/collector/adapters/claude-code/transcript/waitingTexts.ts
3723
- import path8 from "node:path";
3751
+ // src/collector/adapters/claude-code/transcript/lastMessages.ts
3752
+ import path9 from "node:path";
3724
3753
 
3725
3754
  // src/collector/files/readOnlyIo.ts
3726
3755
  import { constants as constants4 } from "node:fs";
@@ -3774,7 +3803,7 @@ function isMissing2(error) {
3774
3803
  return code === "ENOENT" || code === "ENOTDIR";
3775
3804
  }
3776
3805
 
3777
- // src/collector/adapters/claude-code/transcript/lastAsk.ts
3806
+ // src/collector/adapters/claude-code/transcript/transcriptLines.ts
3778
3807
  function isRecord3(value) {
3779
3808
  return typeof value === "object" && value !== null && !Array.isArray(value);
3780
3809
  }
@@ -3800,6 +3829,112 @@ function linesOf(tail, fromStart) {
3800
3829
  }
3801
3830
  return lines;
3802
3831
  }
3832
+
3833
+ // src/collector/adapters/claude-code/transcript/lastSaid.ts
3834
+ var PART_BREAK = "\n\n";
3835
+ function textsOf(line) {
3836
+ if (line.type !== "assistant" || line.isSidechain === true || line.isMeta === true) return [];
3837
+ const texts = [];
3838
+ for (const block of blocksOf(line)) {
3839
+ const text4 = block.type === "text" ? nonEmpty(block.text) : null;
3840
+ if (text4 !== null) texts.push(text4);
3841
+ }
3842
+ return texts;
3843
+ }
3844
+ function messageIdOf(line) {
3845
+ return isRecord3(line.message) ? nonEmpty(line.message.id) : null;
3846
+ }
3847
+ function lastSaidInTail(tail, fromStart) {
3848
+ const lines = linesOf(tail, fromStart);
3849
+ const newest2 = lines.findLastIndex((line) => textsOf(line).length > 0);
3850
+ if (newest2 === -1) return { text: null, reason: fromStart ? "nothing-yet" : "too-far-back" };
3851
+ const parts = [textsOf(lines[newest2])];
3852
+ const messageId = messageIdOf(lines[newest2]);
3853
+ if (messageId === null) return { text: parts[0].join(PART_BREAK) };
3854
+ let index = newest2 - 1;
3855
+ for (; index >= 0; index -= 1) {
3856
+ const line = lines[index];
3857
+ if (line.isSidechain === true) continue;
3858
+ if (line.type === "user") break;
3859
+ if (line.type !== "assistant") continue;
3860
+ if (messageIdOf(line) !== messageId) break;
3861
+ parts.unshift(textsOf(line));
3862
+ }
3863
+ const text4 = parts.flat().join(PART_BREAK);
3864
+ return index < 0 && !fromStart ? { text: text4, startCut: true } : { text: text4 };
3865
+ }
3866
+
3867
+ // src/collector/adapters/claude-code/transcript/transcriptFile.ts
3868
+ import path7 from "node:path";
3869
+ var TRANSCRIPT_TAIL_BYTES = 256 * 1024;
3870
+ var SESSION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
3871
+ function isSessionId(value) {
3872
+ return typeof value === "string" && SESSION_ID.test(value);
3873
+ }
3874
+ var ID_PREFIX = "claude-code:";
3875
+ function sessionIdOf(session) {
3876
+ if (session.source !== "claude-code" || !session.id.startsWith(ID_PREFIX)) return null;
3877
+ const id = session.id.slice(ID_PREFIX.length);
3878
+ return isSessionId(id) ? id : null;
3879
+ }
3880
+ function projectFolderName(cwd) {
3881
+ return cwd.replace(/[^A-Za-z0-9]/g, "-");
3882
+ }
3883
+ function fileName(sessionId) {
3884
+ return `${sessionId}.jsonl`;
3885
+ }
3886
+ async function findTranscript(projectsDir, sessionId, cwd, io) {
3887
+ if (!isSessionId(sessionId)) return null;
3888
+ const name = fileName(sessionId);
3889
+ const isFile2 = async (file) => {
3890
+ try {
3891
+ return (await io.lstat(file)).kind === "file";
3892
+ } catch {
3893
+ return false;
3894
+ }
3895
+ };
3896
+ if (cwd !== null && cwd !== "") {
3897
+ const likely = path7.join(projectsDir, projectFolderName(cwd), name);
3898
+ if (await isFile2(likely)) return likely;
3899
+ }
3900
+ let folders;
3901
+ try {
3902
+ folders = await io.readdir(projectsDir);
3903
+ } catch {
3904
+ return null;
3905
+ }
3906
+ const candidates = folders.filter((folder) => folder !== "" && !folder.startsWith(".")).sort().map((folder) => path7.join(projectsDir, folder, name));
3907
+ const found = await Promise.all(candidates.map(isFile2));
3908
+ return candidates.find((_, index) => found[index]) ?? null;
3909
+ }
3910
+ function stampOf(info) {
3911
+ return { size: info.size, mtimeMs: info.mtimeMs, ino: info.ino };
3912
+ }
3913
+ function sameStamp(a, b) {
3914
+ return a.size === b.size && a.mtimeMs === b.mtimeMs && a.ino === b.ino;
3915
+ }
3916
+ async function readTranscriptTail(file, io, known, maxBytes = TRANSCRIPT_TAIL_BYTES) {
3917
+ const open2 = await io.openRegular(file);
3918
+ try {
3919
+ const stamp = stampOf(open2.info);
3920
+ if (known !== null && sameStamp(known, stamp)) return { unchanged: true, stamp };
3921
+ const start = Math.max(0, stamp.size - maxBytes);
3922
+ const bytes3 = await open2.read(start, stamp.size - start);
3923
+ return {
3924
+ unchanged: false,
3925
+ stamp,
3926
+ tail: new TextDecoder("utf-8").decode(bytes3),
3927
+ fromStart: start === 0
3928
+ };
3929
+ } finally {
3930
+ await open2.close();
3931
+ }
3932
+ }
3933
+
3934
+ // src/collector/adapters/claude-code/transcript/waitingTexts.ts
3935
+ import path8 from "node:path";
3936
+
3937
+ // src/collector/adapters/claude-code/transcript/lastAsk.ts
3803
3938
  function isPrompt(line) {
3804
3939
  if (line.isMeta === true) return false;
3805
3940
  const message = isRecord3(line.message) ? line.message : null;
@@ -3898,79 +4033,12 @@ function askedInTail(tail, fromStart, cwd) {
3898
4033
  return waitingText(describeToolUse(pending.name, pending.input, cwd));
3899
4034
  }
3900
4035
 
3901
- // src/collector/adapters/claude-code/transcript/transcriptFile.ts
3902
- import path7 from "node:path";
3903
- var TRANSCRIPT_TAIL_BYTES = 256 * 1024;
3904
- var SESSION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
3905
- function isSessionId(value) {
3906
- return typeof value === "string" && SESSION_ID.test(value);
3907
- }
3908
- function projectFolderName(cwd) {
3909
- return cwd.replace(/[^A-Za-z0-9]/g, "-");
3910
- }
3911
- function fileName(sessionId) {
3912
- return `${sessionId}.jsonl`;
3913
- }
3914
- async function findTranscript(projectsDir, sessionId, cwd, io) {
3915
- if (!isSessionId(sessionId)) return null;
3916
- const name = fileName(sessionId);
3917
- const isFile2 = async (file) => {
3918
- try {
3919
- return (await io.lstat(file)).kind === "file";
3920
- } catch {
3921
- return false;
3922
- }
3923
- };
3924
- if (cwd !== null && cwd !== "") {
3925
- const likely = path7.join(projectsDir, projectFolderName(cwd), name);
3926
- if (await isFile2(likely)) return likely;
3927
- }
3928
- let folders;
3929
- try {
3930
- folders = await io.readdir(projectsDir);
3931
- } catch {
3932
- return null;
3933
- }
3934
- const candidates = folders.filter((folder) => folder !== "" && !folder.startsWith(".")).sort().map((folder) => path7.join(projectsDir, folder, name));
3935
- const found = await Promise.all(candidates.map(isFile2));
3936
- return candidates.find((_, index) => found[index]) ?? null;
3937
- }
3938
- function stampOf(info) {
3939
- return { size: info.size, mtimeMs: info.mtimeMs, ino: info.ino };
3940
- }
3941
- function sameStamp(a, b) {
3942
- return a.size === b.size && a.mtimeMs === b.mtimeMs && a.ino === b.ino;
3943
- }
3944
- async function readTranscriptTail(file, io, known, maxBytes = TRANSCRIPT_TAIL_BYTES) {
3945
- const open2 = await io.openRegular(file);
3946
- try {
3947
- const stamp = stampOf(open2.info);
3948
- if (known !== null && sameStamp(known, stamp)) return { unchanged: true, stamp };
3949
- const start = Math.max(0, stamp.size - maxBytes);
3950
- const bytes3 = await open2.read(start, stamp.size - start);
3951
- return {
3952
- unchanged: false,
3953
- stamp,
3954
- tail: new TextDecoder("utf-8").decode(bytes3),
3955
- fromStart: start === 0
3956
- };
3957
- } finally {
3958
- await open2.close();
3959
- }
3960
- }
3961
-
3962
4036
  // src/collector/adapters/claude-code/transcript/waitingTexts.ts
3963
4037
  var WAITING_TEXT_ENV = "AGENT_LOOKOUT_WAITING_TEXT";
3964
4038
  function waitingTextOff(env) {
3965
4039
  return env[WAITING_TEXT_ENV]?.trim().toLowerCase() === "off";
3966
4040
  }
3967
4041
  var LOOK_AGAIN_MS = 1e4;
3968
- var ID_PREFIX = "claude-code:";
3969
- function sessionIdOf(session) {
3970
- if (session.source !== "claude-code" || !session.id.startsWith(ID_PREFIX)) return null;
3971
- const id = session.id.slice(ID_PREFIX.length);
3972
- return isSessionId(id) ? id : null;
3973
- }
3974
4042
  function createWaitingTextReader(options) {
3975
4043
  const io = options.io ?? nodeIo2;
3976
4044
  const now = options.now ?? Date.now;
@@ -4028,6 +4096,160 @@ function createWaitingTextReader(options) {
4028
4096
  };
4029
4097
  }
4030
4098
 
4099
+ // src/collector/adapters/claude-code/transcript/lastMessages.ts
4100
+ var ANSWER_AGAIN_MS = 1e3;
4101
+ var READS_PER_SECOND = 4;
4102
+ var MAX_KEPT = 8;
4103
+ var KEEP_MS = 15e3;
4104
+ var NOT_FOUND = { message: null, reason: "not-found" };
4105
+ var UNREADABLE = { message: null, reason: "unreadable" };
4106
+ function runLater(run, ms) {
4107
+ setTimeout(run, ms).unref();
4108
+ }
4109
+ function answerFrom(tail, fromStart) {
4110
+ const said = lastSaidInTail(tail, fromStart);
4111
+ if (said.text === null) return { message: null, reason: said.reason };
4112
+ const shown = messageText(said.text);
4113
+ if (shown === void 0) {
4114
+ return { message: null, reason: fromStart ? "nothing-yet" : "too-far-back" };
4115
+ }
4116
+ return { message: { text: shown.text, cut: shown.cut || said.startCut === true } };
4117
+ }
4118
+ function createLastMessageReader(options) {
4119
+ const io = options.io ?? nodeIo2;
4120
+ const now = options.now ?? Date.now;
4121
+ const later = options.later ?? runLater;
4122
+ const projectsDir = path9.join(options.claudeHome, "projects");
4123
+ const listed = /* @__PURE__ */ new Map();
4124
+ const kept = /* @__PURE__ */ new Map();
4125
+ let reads = [];
4126
+ let dropSet = false;
4127
+ function dropOld(at) {
4128
+ for (const [id, entry] of kept) {
4129
+ if (at - entry.askedAt >= KEEP_MS) kept.delete(id);
4130
+ }
4131
+ }
4132
+ function dropOnTime(at) {
4133
+ if (dropSet || kept.size === 0) return;
4134
+ const oldest = Math.min(...[...kept.values()].map((entry) => entry.askedAt));
4135
+ dropSet = true;
4136
+ later(
4137
+ () => {
4138
+ dropSet = false;
4139
+ const then = now();
4140
+ dropOld(then);
4141
+ dropOnTime(then);
4142
+ },
4143
+ Math.max(0, oldest + KEEP_MS - at)
4144
+ );
4145
+ }
4146
+ function mayRead(at) {
4147
+ reads = reads.filter((started) => at - started < 1e3 && started <= at);
4148
+ if (reads.length >= READS_PER_SECOND) return false;
4149
+ reads.push(at);
4150
+ return true;
4151
+ }
4152
+ function makeRoom() {
4153
+ while (kept.size >= MAX_KEPT) {
4154
+ let leastRecent;
4155
+ for (const pair of kept) {
4156
+ if (leastRecent === void 0 || pair[1].askedAt < leastRecent[1].askedAt) {
4157
+ leastRecent = pair;
4158
+ }
4159
+ }
4160
+ if (leastRecent === void 0) return;
4161
+ kept.delete(leastRecent[0]);
4162
+ }
4163
+ }
4164
+ async function check(entry, target, at) {
4165
+ try {
4166
+ if (entry.file === null) {
4167
+ entry.lookedAt = at;
4168
+ entry.file = await findTranscript(projectsDir, target.sessionId, target.cwd, io);
4169
+ if (entry.file === null) return NOT_FOUND;
4170
+ }
4171
+ const read = await readTranscriptTail(entry.file, io, entry.last?.stamp ?? null);
4172
+ if (read.unchanged) return entry.last?.answer ?? UNREADABLE;
4173
+ const answer2 = answerFrom(read.tail, read.fromStart);
4174
+ entry.last = { stamp: read.stamp, answer: answer2 };
4175
+ return answer2;
4176
+ } catch (error) {
4177
+ entry.last = null;
4178
+ if (isMissing2(error)) {
4179
+ entry.file = null;
4180
+ entry.lookedAt = at;
4181
+ return NOT_FOUND;
4182
+ }
4183
+ return UNREADABLE;
4184
+ }
4185
+ }
4186
+ return {
4187
+ read(id) {
4188
+ const target = listed.get(id);
4189
+ if (target === void 0) return Promise.resolve(null);
4190
+ if (target === null) return Promise.resolve(NOT_FOUND);
4191
+ const at = now();
4192
+ dropOld(at);
4193
+ const entry = kept.get(id);
4194
+ if (entry !== void 0) {
4195
+ entry.askedAt = at;
4196
+ dropOnTime(at);
4197
+ if (entry.checking !== null) return entry.checking;
4198
+ if (entry.answer !== null && at - entry.checkedAt < ANSWER_AGAIN_MS) {
4199
+ return Promise.resolve(entry.answer);
4200
+ }
4201
+ if (entry.answer !== null && entry.file === null && entry.lookedAt !== null && at - entry.lookedAt < LOOK_AGAIN_MS) {
4202
+ return Promise.resolve(entry.answer);
4203
+ }
4204
+ }
4205
+ if (!mayRead(at)) return Promise.resolve("busy");
4206
+ let fresh = entry;
4207
+ if (fresh === void 0) {
4208
+ makeRoom();
4209
+ fresh = {
4210
+ file: null,
4211
+ lookedAt: null,
4212
+ last: null,
4213
+ answer: null,
4214
+ checkedAt: at,
4215
+ askedAt: at,
4216
+ checking: null
4217
+ };
4218
+ kept.set(id, fresh);
4219
+ dropOnTime(at);
4220
+ }
4221
+ const reading = fresh;
4222
+ const checking = check(reading, target, at).then((answer2) => {
4223
+ reading.answer = answer2;
4224
+ reading.checkedAt = at;
4225
+ reading.checking = null;
4226
+ return answer2;
4227
+ });
4228
+ reading.checking = checking;
4229
+ return checking;
4230
+ },
4231
+ keep(sessions) {
4232
+ listed.clear();
4233
+ for (const session of sessions) {
4234
+ const sessionId = sessionIdOf(session);
4235
+ listed.set(session.id, sessionId === null ? null : { sessionId, cwd: session.cwd });
4236
+ }
4237
+ for (const id of kept.keys()) {
4238
+ if (!listed.has(id)) kept.delete(id);
4239
+ }
4240
+ dropOld(now());
4241
+ },
4242
+ forget() {
4243
+ listed.clear();
4244
+ kept.clear();
4245
+ reads = [];
4246
+ },
4247
+ get size() {
4248
+ return kept.size;
4249
+ }
4250
+ };
4251
+ }
4252
+
4031
4253
  // src/collector/adapters/claude-code/index.ts
4032
4254
  var CLAUDE_HOME_ENV = "AGENT_LOOKOUT_CLAUDE_HOME";
4033
4255
  var CLAUDE_FEED_ENV = "AGENT_LOOKOUT_CLAUDE_FEED";
@@ -4056,6 +4278,10 @@ var CLAUDE_CODE_CAPABILITIES = {
4056
4278
  level: "no",
4057
4279
  reason: "The file Agent Lookout reads is not rewritten as a session works."
4058
4280
  },
4281
+ tokens: {
4282
+ level: "no",
4283
+ reason: "Agent Lookout does not read the token counts in its transcripts yet."
4284
+ },
4059
4285
  stop: {
4060
4286
  level: "partly",
4061
4287
  reason: "In a terminal, in VS Code and for background jobs. Not in the desktop app."
@@ -4128,6 +4354,8 @@ function createClaudeCodeAdapter(options = {}) {
4128
4354
  const homeName = tildify(claudeHome, homeDir, platform);
4129
4355
  const registryName = tildify(sessionsDir, homeDir, platform);
4130
4356
  const waitingTexts = transcriptsOff ? null : createWaitingTextReader({ claudeHome, io: options.transcriptIo, now });
4357
+ const lastMessagesOff = lastMessageOff(env);
4358
+ const lastMessages = transcriptsOff || lastMessagesOff ? null : createLastMessageReader({ claudeHome, io: options.transcriptIo, now });
4131
4359
  let lastDue = null;
4132
4360
  let lastRead = null;
4133
4361
  let feedSoon = false;
@@ -4149,7 +4377,7 @@ function createClaudeCodeAdapter(options = {}) {
4149
4377
  { label: "Command run", value: commandRun },
4150
4378
  {
4151
4379
  label: "Transcript read",
4152
- value: transcriptsOff ? "Off" : "last message of a waiting session"
4380
+ value: transcriptsOff ? "Off" : lastMessagesOff ? "last message of a waiting session" : "last message of a waiting session, and of one whose details are open"
4153
4381
  }
4154
4382
  ];
4155
4383
  }
@@ -4245,12 +4473,12 @@ function createClaudeCodeAdapter(options = {}) {
4245
4473
  }
4246
4474
  reading ??= await readRegistryNow(checkedAt);
4247
4475
  const read = lastRead;
4248
- const answer = read?.feed.ok ? read.feed.entries : null;
4476
+ const answer2 = read?.feed.ok ? read.feed.entries : null;
4249
4477
  const problem = problemWith(reading, read);
4250
4478
  const { registry, live: live2 } = reading;
4251
4479
  const offered = (result) => {
4252
4480
  if (!stops) return result;
4253
- const found = withStopOffers(result.sessions, live2, answer, {
4481
+ const found = withStopOffers(result.sessions, live2, answer2, {
4254
4482
  sessionsDir,
4255
4483
  background: !neverRun
4256
4484
  });
@@ -4259,7 +4487,7 @@ function createClaudeCodeAdapter(options = {}) {
4259
4487
  };
4260
4488
  if (options.panes || options.terminals) {
4261
4489
  const pids = new Set(live2.map((entry) => entry.pid));
4262
- for (const entry of answer ?? []) {
4490
+ for (const entry of answer2 ?? []) {
4263
4491
  if (entry.pid !== void 0 && isAlive(entry.pid)) pids.add(entry.pid);
4264
4492
  }
4265
4493
  await Promise.all([options.panes?.look([...pids]), options.terminals?.look([...pids])]);
@@ -4279,16 +4507,16 @@ function createClaudeCodeAdapter(options = {}) {
4279
4507
  ...advice2 !== void 0 && { advice: advice2 },
4280
4508
  checkedAt
4281
4509
  });
4282
- if (answer !== null && read !== null && problem === null) {
4283
- const listedPids = new Set(answer.map((entry) => entry.pid));
4284
- const listedSessionIds = new Set(answer.map((entry) => entry.sessionId));
4510
+ if (answer2 !== null && read !== null && problem === null) {
4511
+ const listedPids = new Set(answer2.map((entry) => entry.pid));
4512
+ const listedSessionIds = new Set(answer2.map((entry) => entry.sessionId));
4285
4513
  const tooNewSince = read.askedAt - NEW_SESSION_GRACE_MS;
4286
4514
  const fromRegistry = live2.filter(
4287
4515
  (entry) => listedPids.has(entry.pid) || listedSessionIds.has(entry.sessionId) || startedAfter(entry, tooNewSince, checkedAt)
4288
4516
  );
4289
4517
  const livePids = new Set(live2.map((entry) => entry.pid));
4290
4518
  const liveSessionIds = new Set(live2.map((entry) => entry.sessionId));
4291
- const jobs = answer.filter(
4519
+ const jobs = answer2.filter(
4292
4520
  (entry) => (entry.id !== void 0 || entry.state !== void 0) && !(entry.pid !== void 0 && livePids.has(entry.pid)) && !(entry.sessionId !== void 0 && liveSessionIds.has(entry.sessionId))
4293
4521
  ).map((entry) => {
4294
4522
  const gone = entry.pid !== void 0 && !isAlive(entry.pid);
@@ -4309,9 +4537,9 @@ function createClaudeCodeAdapter(options = {}) {
4309
4537
  basis: "registry+feed"
4310
4538
  });
4311
4539
  }
4312
- if (answer !== null && problem !== null) {
4540
+ if (answer2 !== null && problem !== null) {
4313
4541
  const entries = registry.readable ? registry.entries : void 0;
4314
- const sessions = answer.map(
4542
+ const sessions = answer2.map(
4315
4543
  (entry) => sessionFromFeed(
4316
4544
  entry,
4317
4545
  entry.pid !== void 0 ? entries?.get(entry.pid) : void 0,
@@ -4365,9 +4593,12 @@ function createClaudeCodeAdapter(options = {}) {
4365
4593
  lookingIn: neverRun ? `Looking for sessions in ${registryName}.` : `Looking for sessions in ${registryName} and with claude ${FEED_ARGS.join(" ")}.`,
4366
4594
  async poll() {
4367
4595
  try {
4368
- return await withWaitingText(await poll());
4596
+ const result = await withWaitingText(await poll());
4597
+ lastMessages?.keep(result.sessions);
4598
+ return result;
4369
4599
  } catch {
4370
4600
  await waitingTexts?.annotate([]);
4601
+ lastMessages?.forget();
4371
4602
  stops?.set(/* @__PURE__ */ new Map());
4372
4603
  return {
4373
4604
  health: {
@@ -4385,6 +4616,19 @@ function createClaudeCodeAdapter(options = {}) {
4385
4616
  sessions: []
4386
4617
  };
4387
4618
  }
4619
+ },
4620
+ async lastMessage(sessionId) {
4621
+ if (lastMessages === null) {
4622
+ return {
4623
+ message: null,
4624
+ reason: "off",
4625
+ setting: transcriptsOff ? WAITING_TEXT_ENV : LAST_MESSAGE_ENV
4626
+ };
4627
+ }
4628
+ return lastMessages.read(sessionId);
4629
+ },
4630
+ get lastMessagesKept() {
4631
+ return lastMessages?.size ?? 0;
4388
4632
  }
4389
4633
  };
4390
4634
  }
@@ -4400,8 +4644,8 @@ function parseProcessParents(stdout) {
4400
4644
  return parents;
4401
4645
  }
4402
4646
  var readProcessParentsWithPs = async () => {
4403
- const answer = await runPs(PS_PARENTS_ARGS);
4404
- return answer.ok ? parseProcessParents(answer.stdout) : /* @__PURE__ */ new Map();
4647
+ const answer2 = await runPs(PS_PARENTS_ARGS);
4648
+ return answer2.ok ? parseProcessParents(answer2.stdout) : /* @__PURE__ */ new Map();
4405
4649
  };
4406
4650
 
4407
4651
  // src/collector/actions/stopSession.ts
@@ -4869,7 +5113,7 @@ function createAnswerer(options) {
4869
5113
  };
4870
5114
  }
4871
5115
  function createAnswerRoute(options) {
4872
- const answer = createAnswerer(options);
5116
+ const answer2 = createAnswerer(options);
4873
5117
  return async function answerPermission(req) {
4874
5118
  const refused = answerRefusalFor(req);
4875
5119
  if (refused) {
@@ -4888,7 +5132,7 @@ function createAnswerRoute(options) {
4888
5132
  'The body must be {"sessionId": "...", "requestId": "...", "decision": "allow" or "deny"} and nothing else.'
4889
5133
  );
4890
5134
  }
4891
- const outcome = await answer(asked.sessionId, asked.requestId, asked.decision);
5135
+ const outcome = await answer2(asked.sessionId, asked.requestId, asked.decision);
4892
5136
  if (outcome !== "answered") return answerFailed(outcome);
4893
5137
  return {
4894
5138
  status: 200,
@@ -5187,7 +5431,7 @@ function createHeldAsks(options) {
5187
5431
  if (inFlight === run) inFlight = null;
5188
5432
  }
5189
5433
  }
5190
- async function answer(sessionId, requestId, decision) {
5434
+ async function answer2(sessionId, requestId, decision) {
5191
5435
  const entry = held.get(sessionId);
5192
5436
  if (!entry || entry.requestId !== requestId || !entry.confirmed) return "no-ask";
5193
5437
  if (decision === "allow" && !entry.shown.allow) return "not-allowable";
@@ -5219,7 +5463,7 @@ function createHeldAsks(options) {
5219
5463
  const verdict = verdictOf(entry);
5220
5464
  if (verdict === null) return;
5221
5465
  entry.byRule = true;
5222
- void answer(entry.sessionId, entry.requestId, verdict.decision).then((outcome) => {
5466
+ void answer2(entry.sessionId, entry.requestId, verdict.decision).then((outcome) => {
5223
5467
  if (outcome === "answered") rules.answered(request2, verdict);
5224
5468
  });
5225
5469
  }
@@ -5276,7 +5520,7 @@ function createHeldAsks(options) {
5276
5520
  void check();
5277
5521
  },
5278
5522
  check,
5279
- answer,
5523
+ answer: answer2,
5280
5524
  withAsks(snapshot) {
5281
5525
  const answering = status();
5282
5526
  if (held.size === 0) return { ...snapshot, answering };
@@ -5339,7 +5583,7 @@ function createHeldAsks(options) {
5339
5583
  import { chmod, lstat as lstat3, mkdir, stat as stat4, unlink } from "node:fs/promises";
5340
5584
  import http from "node:http";
5341
5585
  import net from "node:net";
5342
- import path9 from "node:path";
5586
+ import path10 from "node:path";
5343
5587
  var HOOK_PATH = "/hooks/permission-request";
5344
5588
  var HOOK_HEADER = "x-agent-lookout-hook";
5345
5589
  var HOOK_HEADER_VALUE = "permission-request";
@@ -5412,7 +5656,7 @@ function createHookSocket(options) {
5412
5656
  const maxBody = options.maxBodyBytes ?? MAX_HOOK_BODY_BYTES;
5413
5657
  const bodyTimeoutMs = options.bodyTimeoutMs ?? HOOK_BODY_TIMEOUT_MS;
5414
5658
  const uid = options.uid ?? process.getuid?.() ?? -1;
5415
- const folder = path9.dirname(socketPath);
5659
+ const folder = path10.dirname(socketPath);
5416
5660
  let server = null;
5417
5661
  let made = null;
5418
5662
  async function handle(req, res) {
@@ -5535,12 +5779,12 @@ function createHookSocket(options) {
5535
5779
 
5536
5780
  // src/collector/answers/registryStatus.ts
5537
5781
  import os4 from "node:os";
5538
- import path10 from "node:path";
5782
+ import path11 from "node:path";
5539
5783
  function createRegistryStatus(options) {
5540
5784
  const homeDir = options.homeDir ?? os4.homedir();
5541
5785
  const readFile2 = options.readFile ?? readRegularFile;
5542
- const home = options.env[CLAUDE_HOME_ENV]?.trim() || path10.join(homeDir, ".claude");
5543
- const sessionsDir = path10.join(path10.resolve(home), "sessions");
5786
+ const home = options.env[CLAUDE_HOME_ENV]?.trim() || path11.join(homeDir, ".claude");
5787
+ const sessionsDir = path11.join(path11.resolve(home), "sessions");
5544
5788
  return {
5545
5789
  async statusOf(sessionId) {
5546
5790
  const unknown = { status: "unknown" };
@@ -5551,7 +5795,7 @@ function createRegistryStatus(options) {
5551
5795
  const ownId = sessionId.slice("claude-code:".length);
5552
5796
  let content;
5553
5797
  try {
5554
- content = await readFile2(path10.join(sessionsDir, `${pid}.json`));
5798
+ content = await readFile2(path11.join(sessionsDir, `${pid}.json`));
5555
5799
  } catch {
5556
5800
  return unknown;
5557
5801
  }
@@ -5825,18 +6069,18 @@ function namesOwnFiles(words, ownPaths = [], folder) {
5825
6069
  (part) => from !== void 0 && !part.startsWith("/") && !part.startsWith("-") ? [part, `${from}/${part}`] : [part]
5826
6070
  );
5827
6071
  return read.some((part) => {
5828
- const path27 = plainPath(part);
5829
- if (path27 === BY_NUMBER || path27.startsWith(`${BY_NUMBER}/`)) return true;
5830
- if (own.some((name) => path27 === name || path27.startsWith(`${name}/`))) return true;
5831
- if (above.has(path27)) return true;
5832
- return ownNames.some((name) => path27 === name || path27.endsWith(`/${name}`));
6072
+ const path28 = plainPath(part);
6073
+ if (path28 === BY_NUMBER || path28.startsWith(`${BY_NUMBER}/`)) return true;
6074
+ if (own.some((name) => path28 === name || path28.startsWith(`${name}/`))) return true;
6075
+ if (above.has(path28)) return true;
6076
+ return ownNames.some((name) => path28 === name || path28.endsWith(`/${name}`));
5833
6077
  });
5834
6078
  });
5835
6079
  }
5836
6080
  function folderReachesOwnFiles(folder, ownPaths = []) {
5837
6081
  const own = ownPaths.filter((name) => name.startsWith("/")).map(plainPath);
5838
- const path27 = plainPath(folder);
5839
- return own.some((name) => name === path27 || name.startsWith(`${path27 === "/" ? "" : path27}/`));
6082
+ const path28 = plainPath(folder);
6083
+ return own.some((name) => name === path28 || name.startsWith(`${path28 === "/" ? "" : path28}/`));
5840
6084
  }
5841
6085
  var PLAIN_WORD = /^[A-Za-z0-9_\-.,/:=+@%]+$/;
5842
6086
  var ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
@@ -6218,7 +6462,7 @@ function createAnswering(options) {
6218
6462
  let problem = null;
6219
6463
  let listening = false;
6220
6464
  const answerRoute = createAnswerRoute({ poller, events, asks, now: options.now });
6221
- const answer = createAnswerer({ poller, events, asks, now: options.now });
6465
+ const answer2 = createAnswerer({ poller, events, asks, now: options.now });
6222
6466
  return {
6223
6467
  async start() {
6224
6468
  const started = await socket.start();
@@ -6252,7 +6496,7 @@ function createAnswering(options) {
6252
6496
  return answerRefusalFor(req) ?? refusal(405, "This address only answers GET requests.", { Allow: "GET" });
6253
6497
  },
6254
6498
  // The same answer, from the host's own process. With no socket nothing is held.
6255
- answer: async (sessionId, requestId, decision) => problem === null ? answer(sessionId, requestId, decision) : "unavailable",
6499
+ answer: async (sessionId, requestId, decision) => problem === null ? answer2(sessionId, requestId, decision) : "unavailable",
6256
6500
  check: () => asks.check(),
6257
6501
  ruleAnswers: () => ruleAnswers.recent()
6258
6502
  };
@@ -6483,7 +6727,7 @@ function waitTotals(input) {
6483
6727
 
6484
6728
  // src/collector/adapters/antigravity/index.ts
6485
6729
  import os5 from "node:os";
6486
- import path14 from "node:path";
6730
+ import path15 from "node:path";
6487
6731
 
6488
6732
  // src/core/mapping/antigravityLiveness.ts
6489
6733
  function holdConversations(processes, conversations, listedAt = null) {
@@ -6522,14 +6766,15 @@ var SOURCE2 = {
6522
6766
  backend: "server.go",
6523
6767
  created: "server.go",
6524
6768
  streaming: "conversation_manager.go",
6525
- confirmation: "tool_confirmation_manager.go"
6769
+ asking: "tool_confirmation_manager.go",
6770
+ answered: "input_loop.go"
6526
6771
  };
6527
6772
  var UUID = "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}";
6528
6773
  var OPENED = new RegExp(`^(Created|Streaming) conversation (${UUID})$`);
6529
6774
  var BACKEND = /^Creating CLI server backend: (?:.* )?workspaceDirs=\[([^\]\s]+)\](?: |$)/;
6530
6775
  var ASKING = /^Surfacing tool confirmation: "([A-Za-z]{1,64})" at step (\d{1,9})$/;
6531
6776
  var ANSWERED = new RegExp(
6532
- `^Responding to tool confirmation: convID=(${UUID}), stepIdx=(\\d{1,9}), approved=(?:true|false)$`
6777
+ `^Responding to tool confirmation: convID=(${UUID}), stepIdx=(\\d{1,9}), approved=(?:true|false)(?:, .*)?$`
6533
6778
  );
6534
6779
  var ABSOLUTE = /^(?:\/|[A-Za-z]:[\\/])/;
6535
6780
  var UNPRINTABLE = /[\p{Cc}\p{Cf}]/u;
@@ -6547,7 +6792,11 @@ function readAgyLogLine(line) {
6547
6792
  const backend = BACKEND.exec(message);
6548
6793
  return backend !== null && ABSOLUTE.test(backend[1]) && !UNPRINTABLE.test(backend[1]) ? { kind: "folder", folder: backend[1] } : null;
6549
6794
  }
6550
- if (source !== SOURCE2.confirmation) return null;
6795
+ if (source === SOURCE2.answered) {
6796
+ const answered = ANSWERED.exec(message);
6797
+ return answered === null ? null : { kind: "answered", conversation: answered[1].toLowerCase(), step: Number(answered[2]) };
6798
+ }
6799
+ if (source !== SOURCE2.asking) return null;
6551
6800
  const asking = ASKING.exec(message);
6552
6801
  if (asking !== null) {
6553
6802
  const fraction = (glog[6] ?? "0").padEnd(3, "0").slice(0, 3);
@@ -6565,10 +6814,6 @@ function readAgyLogLine(line) {
6565
6814
  }
6566
6815
  };
6567
6816
  }
6568
- const answered = ANSWERED.exec(message);
6569
- if (answered !== null) {
6570
- return { kind: "answered", conversation: answered[1].toLowerCase(), step: Number(answered[2]) };
6571
- }
6572
6817
  return null;
6573
6818
  }
6574
6819
  function emptyAgyLogState() {
@@ -6621,7 +6866,7 @@ function agyLogTimeAt(time, near) {
6621
6866
  }
6622
6867
 
6623
6868
  // src/collector/adapters/antigravity/agyLogs.ts
6624
- import path11 from "node:path";
6869
+ import path12 from "node:path";
6625
6870
  var LOG_DIR = "log";
6626
6871
  var LOG_LIMIT_BYTES = 4 * 1024 * 1024;
6627
6872
  var LINE_LIMIT_BYTES = 64 * 1024;
@@ -6656,7 +6901,7 @@ function matchLogs(processes, logs) {
6656
6901
  function createAgyLogReader(options) {
6657
6902
  const { io } = options;
6658
6903
  const now = options.now ?? Date.now;
6659
- const logDir = path11.join(options.home, LOG_DIR);
6904
+ const logDir = path12.join(options.home, LOG_DIR);
6660
6905
  const cursors = /* @__PURE__ */ new Map();
6661
6906
  let listed = /* @__PURE__ */ new Map();
6662
6907
  let listedFor = "";
@@ -6753,7 +6998,7 @@ function createAgyLogReader(options) {
6753
6998
  states.push(null);
6754
6999
  continue;
6755
7000
  }
6756
- const file = path11.join(logDir, name);
7001
+ const file = path12.join(logDir, name);
6757
7002
  kept.add(file);
6758
7003
  try {
6759
7004
  const info = await io.lstat(file);
@@ -6871,12 +7116,12 @@ function createAgyProcessReader(options = {}) {
6871
7116
  const unseen = rows.filter((row) => !known.has(keyOf(row)));
6872
7117
  const ended = /* @__PURE__ */ new Set();
6873
7118
  if (unseen.length > 0) {
6874
- const answer = await run(agyCommandLineArgs(unseen.map((row) => row.pid)));
6875
- const lines = parseCommandLines(answer.stdout);
7119
+ const answer2 = await run(agyCommandLineArgs(unseen.map((row) => row.pid)));
7120
+ const lines = parseCommandLines(answer2.stdout);
6876
7121
  for (const row of unseen) {
6877
7122
  const line = lines.get(row.pid);
6878
7123
  if (line !== void 0) known.set(keyOf(row), readAgyCommandLine(line));
6879
- else if (answer.ok || lines.size > 0) ended.add(row.pid);
7124
+ else if (answer2.ok || lines.size > 0) ended.add(row.pid);
6880
7125
  }
6881
7126
  }
6882
7127
  const running = /* @__PURE__ */ new Map();
@@ -6916,7 +7161,7 @@ function createAgyProcessReader(options = {}) {
6916
7161
  }
6917
7162
 
6918
7163
  // src/collector/adapters/antigravity/conversations.ts
6919
- import path12 from "node:path";
7164
+ import path13 from "node:path";
6920
7165
  var BRAIN_DIR = "brain";
6921
7166
  var CONVERSATIONS_DIR = "conversations";
6922
7167
  var TRANSCRIPT_IN_FOLDER = [".system_generated", "logs", "transcript.jsonl"];
@@ -6932,15 +7177,15 @@ function conversationIdOfDatabase(name) {
6932
7177
  }
6933
7178
  function createConversationFinder(options) {
6934
7179
  const { io } = options;
6935
- const brainDir = path12.join(options.home, BRAIN_DIR);
6936
- const databaseDir = path12.join(options.home, CONVERSATIONS_DIR);
7180
+ const brainDir = path13.join(options.home, BRAIN_DIR);
7181
+ const databaseDir = path13.join(options.home, CONVERSATIONS_DIR);
6937
7182
  const oldRefreshMs = options.oldRefreshMs ?? OLD_REFRESH_MS;
6938
7183
  const known = /* @__PURE__ */ new Map();
6939
7184
  let oldCheckedAt = null;
6940
7185
  async function lookAt(id) {
6941
- const transcript = path12.join(brainDir, id, ...TRANSCRIPT_IN_FOLDER);
7186
+ const transcript = path13.join(brainDir, id, ...TRANSCRIPT_IN_FOLDER);
6942
7187
  const infos = await Promise.all(
6943
- [transcript, path12.join(databaseDir, `${id}.db`), path12.join(databaseDir, `${id}.db-wal`)].map(
7188
+ [transcript, path13.join(databaseDir, `${id}.db`), path13.join(databaseDir, `${id}.db-wal`)].map(
6944
7189
  (file) => io.lstat(file).catch(() => null)
6945
7190
  )
6946
7191
  );
@@ -6996,7 +7241,7 @@ function writtenRecently(files, now) {
6996
7241
  }
6997
7242
 
6998
7243
  // src/collector/adapters/antigravity/annotations.ts
6999
- import path13 from "node:path";
7244
+ import path14 from "node:path";
7000
7245
  var ANNOTATIONS_DIR = "annotations";
7001
7246
  var ANNOTATION_LIMIT_BYTES = 4 * 1024;
7002
7247
  var TITLE_FIELD = /^title[ \t]*:[ \t]*"((?:[^"\\\n]|\\.)*)"[ \t]*$/m;
@@ -7068,10 +7313,10 @@ function readTitle(content) {
7068
7313
  }
7069
7314
  function createTitleReader(options) {
7070
7315
  const { io } = options;
7071
- const dir = path13.join(options.home, ANNOTATIONS_DIR);
7316
+ const dir = path14.join(options.home, ANNOTATIONS_DIR);
7072
7317
  const cache = /* @__PURE__ */ new Map();
7073
7318
  async function read(id) {
7074
- const file = path13.join(dir, `${id}.pbtxt`);
7319
+ const file = path14.join(dir, `${id}.pbtxt`);
7075
7320
  const info = await io.lstat(file);
7076
7321
  const known = cache.get(id);
7077
7322
  if (known && known.ino === info.ino && known.size === info.size && known.mtimeMs === info.mtimeMs) {
@@ -7622,6 +7867,10 @@ var ANTIGRAVITY_CAPABILITIES = {
7622
7867
  reason: "The transcripts name no terminal, and agy documents no link that opens a conversation."
7623
7868
  },
7624
7869
  "quiet-for": { level: "yes" },
7870
+ tokens: {
7871
+ level: "no",
7872
+ reason: "Agent Lookout does not read token counts from agy's transcripts yet."
7873
+ },
7625
7874
  stop: {
7626
7875
  level: "no",
7627
7876
  reason: "No agy program is tied to one conversation surely enough for Agent Lookout to confirm it and stop it."
@@ -7654,8 +7903,8 @@ function createAntigravityAdapter(options = {}) {
7654
7903
  const processes = options.processes ?? createAgyProcessReader({ platform });
7655
7904
  const override = env[ANTIGRAVITY_HOME_ENV]?.trim() || void 0;
7656
7905
  const homeFrom = override ? "agent-lookout" : "default";
7657
- const home = path14.resolve(override ?? path14.join(homeDir, ...DEFAULT_HOME));
7658
- const brainDir = path14.join(home, BRAIN_DIR);
7906
+ const home = path15.resolve(override ?? path15.join(homeDir, ...DEFAULT_HOME));
7907
+ const brainDir = path15.join(home, BRAIN_DIR);
7659
7908
  const homeName = tildify(home, homeDir);
7660
7909
  const brainName = tildify(brainDir, homeDir);
7661
7910
  const finder = createConversationFinder({ home, io, oldRefreshMs: options.oldRefreshMs });
@@ -7820,8 +8069,8 @@ function createAntigravityAdapter(options = {}) {
7820
8069
  basis = BASIS_UNMATCHED;
7821
8070
  }
7822
8071
  if (unknownStep) detail += ` ${UNKNOWN_STEP_NOTE}`;
7823
- const answer = result("ok", detail, every(pollIntervalMs), checkedAt, basis);
7824
- return { ...answer, sessions };
8072
+ const answer2 = result("ok", detail, every(pollIntervalMs), checkedAt, basis);
8073
+ return { ...answer2, sessions };
7825
8074
  }
7826
8075
  return {
7827
8076
  id: SOURCE_ID2,
@@ -7845,7 +8094,7 @@ function createAntigravityAdapter(options = {}) {
7845
8094
 
7846
8095
  // src/collector/adapters/codex/index.ts
7847
8096
  import os6 from "node:os";
7848
- import path16 from "node:path";
8097
+ import path17 from "node:path";
7849
8098
 
7850
8099
  // src/core/mapping/codexMapping.ts
7851
8100
  var TURN_UNDER_WAY = /* @__PURE__ */ new Set(["task_started", "turn_started"]);
@@ -7911,6 +8160,22 @@ function isCodexSessionSource(source, meta = {}) {
7911
8160
  return true;
7912
8161
  }
7913
8162
 
8163
+ // src/core/tokens/tokenCounts.ts
8164
+ function isRecord10(value) {
8165
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8166
+ }
8167
+ function isCount(value) {
8168
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
8169
+ }
8170
+ function tokenCountsOf(value) {
8171
+ if (!isRecord10(value)) return null;
8172
+ const { input, cached, output } = value;
8173
+ if (!isCount(input) || input === 0 || !isCount(output)) return null;
8174
+ if (cached === void 0) return { input, output };
8175
+ if (!isCount(cached) || cached > input) return null;
8176
+ return { input, cached, output };
8177
+ }
8178
+
7914
8179
  // src/collector/adapters/codex/rolloutFile.ts
7915
8180
  var HEAD_LIMIT_BYTES2 = 2 * 1024 * 1024;
7916
8181
  var TAIL_LIMIT_BYTES2 = 8 * 1024 * 1024;
@@ -7926,7 +8191,8 @@ function parseCodexTime(value) {
7926
8191
  var LINE_TIME = /^\{"timestamp":"([^"]{1,64})"/;
7927
8192
  var TURN_HINT = /"type":"(?:task|turn)_/;
7928
8193
  var TURN_TYPE = /^(?:task|turn)_/;
7929
- function isRecord10(value) {
8194
+ var TOKEN_HINT = '"type":"token_count"';
8195
+ function isRecord11(value) {
7930
8196
  return typeof value === "object" && value !== null && !Array.isArray(value);
7931
8197
  }
7932
8198
  function text3(value) {
@@ -7937,7 +8203,7 @@ function label(value) {
7937
8203
  }
7938
8204
  function compactSource(value) {
7939
8205
  if (typeof value === "string") return text3(value);
7940
- if (!isRecord10(value)) return void 0;
8206
+ if (!isRecord11(value)) return void 0;
7941
8207
  return Object.fromEntries(
7942
8208
  Object.entries(value).map(([kind, detail]) => [
7943
8209
  kind,
@@ -7948,7 +8214,7 @@ function compactSource(value) {
7948
8214
  function parse(line) {
7949
8215
  try {
7950
8216
  const value = JSON.parse(line);
7951
- return isRecord10(value) ? value : null;
8217
+ return isRecord11(value) ? value : null;
7952
8218
  } catch {
7953
8219
  return null;
7954
8220
  }
@@ -7956,7 +8222,7 @@ function parse(line) {
7956
8222
  function readMetaLine(line) {
7957
8223
  if (!line.includes('"session_meta"')) return null;
7958
8224
  const value = parse(line);
7959
- if (!value || value.type !== "session_meta" || !isRecord10(value.payload)) return null;
8225
+ if (!value || value.type !== "session_meta" || !isRecord11(value.payload)) return null;
7960
8226
  const payload = value.payload;
7961
8227
  const meta = {
7962
8228
  id: text3(payload.id),
@@ -7976,7 +8242,7 @@ function readMetaLine(line) {
7976
8242
  function readTurnLine(line) {
7977
8243
  if (!line.includes('"event_msg"') || !TURN_HINT.test(line)) return null;
7978
8244
  const value = parse(line);
7979
- if (!value || value.type !== "event_msg" || !isRecord10(value.payload)) return null;
8245
+ if (!value || value.type !== "event_msg" || !isRecord11(value.payload)) return null;
7980
8246
  const turn = value.payload.type;
7981
8247
  if (typeof turn !== "string" || !TURN_TYPE.test(turn)) return null;
7982
8248
  return {
@@ -7985,6 +8251,21 @@ function readTurnLine(line) {
7985
8251
  imported: isImportedCodexTurn(value.payload.turn_id)
7986
8252
  };
7987
8253
  }
8254
+ function readTokenLine(line) {
8255
+ if (!line.includes('"event_msg"') || !line.includes(TOKEN_HINT)) return null;
8256
+ const value = parse(line);
8257
+ if (!value || value.type !== "event_msg" || !isRecord11(value.payload)) return null;
8258
+ if (value.payload.type !== "token_count" || value.payload.info === null) return null;
8259
+ const last = isRecord11(value.payload.info) ? value.payload.info.last_token_usage : void 0;
8260
+ if (!isRecord11(last)) return { tokens: null };
8261
+ return {
8262
+ tokens: tokenCountsOf({
8263
+ input: last.input_tokens,
8264
+ cached: last.cached_input_tokens,
8265
+ output: last.output_tokens
8266
+ })
8267
+ };
8268
+ }
7988
8269
  function lineTime(line) {
7989
8270
  const match = LINE_TIME.exec(line);
7990
8271
  return match ? parseCodexTime(match[1]) : null;
@@ -8020,20 +8301,31 @@ async function scanTail2(file, size) {
8020
8301
  let fragment = Buffer.alloc(0);
8021
8302
  let end = null;
8022
8303
  let lastLineAt = null;
8304
+ let turn = null;
8305
+ let counted2 = null;
8306
+ let lookedBack = false;
8023
8307
  const take = (line) => {
8024
- if (line.trim() === "") return null;
8025
- const turn = readTurnLine(line);
8026
- lastLineAt ??= lineTime(line) ?? turn?.at ?? null;
8027
- return turn;
8028
- };
8029
- const found = (turn) => ({
8030
- lastTurn: turn.turn,
8031
- lastTurnAt: turn.at,
8032
- lastTurnImported: turn.imported,
8308
+ if (line.trim() === "") return false;
8309
+ if (turn === null) {
8310
+ turn = readTurnLine(line);
8311
+ lastLineAt ??= lineTime(line) ?? turn?.at ?? null;
8312
+ }
8313
+ counted2 ??= readTokenLine(line);
8314
+ return turn !== null && counted2 !== null;
8315
+ };
8316
+ const found = (whole) => ({
8317
+ lastTurn: turn !== null ? turn.turn : whole ? null : void 0,
8318
+ lastTurnAt: turn?.at ?? null,
8319
+ lastTurnImported: turn?.imported ?? false,
8033
8320
  lastLineAt,
8321
+ tokens: counted2?.tokens ?? null,
8034
8322
  end
8035
8323
  });
8036
8324
  while (position > 0 && budget2 > 0) {
8325
+ if (turn !== null) {
8326
+ if (lookedBack) break;
8327
+ lookedBack = true;
8328
+ }
8037
8329
  const length = Math.min(TAIL_CHUNK_BYTES2, position, budget2);
8038
8330
  const start = position - length;
8039
8331
  const chunk = await readExactly2(file, start, length);
@@ -8052,22 +8344,18 @@ async function scanTail2(file, size) {
8052
8344
  }
8053
8345
  let newline = cut2 > 0 ? buffer.lastIndexOf(NEWLINE3, cut2 - 1) : -1;
8054
8346
  while (newline !== -1) {
8055
- const turn = take(buffer.toString("utf8", newline + 1, cut2));
8056
- if (turn) return found(turn);
8347
+ if (take(buffer.toString("utf8", newline + 1, cut2))) return found(false);
8057
8348
  cut2 = newline;
8058
8349
  newline = cut2 > 0 ? buffer.lastIndexOf(NEWLINE3, cut2 - 1) : -1;
8059
8350
  }
8060
8351
  if (start === 0) {
8061
- const turn = take(buffer.toString("utf8", 0, cut2));
8062
- if (turn) return found(turn);
8063
- return { lastTurn: null, lastTurnAt: null, lastTurnImported: false, lastLineAt, end };
8352
+ take(buffer.toString("utf8", 0, cut2));
8353
+ return found(true);
8064
8354
  }
8065
8355
  fragment = buffer.subarray(0, cut2);
8066
8356
  }
8067
- if (position === 0) {
8068
- return { lastTurn: null, lastTurnAt: null, lastTurnImported: false, lastLineAt, end: end ?? 0 };
8069
- }
8070
- return { lastTurn: void 0, lastTurnAt: null, lastTurnImported: false, lastLineAt, end };
8357
+ if (position === 0) return { ...found(true), end: end ?? 0 };
8358
+ return found(false);
8071
8359
  }
8072
8360
  function stateOf2(cached) {
8073
8361
  return {
@@ -8075,7 +8363,8 @@ function stateOf2(cached) {
8075
8363
  lastTurn: cached.lastTurn,
8076
8364
  lastTurnAt: cached.lastTurnAt,
8077
8365
  lastTurnImported: cached.lastTurnImported,
8078
- lastLineAt: cached.lastLineAt
8366
+ lastLineAt: cached.lastLineAt,
8367
+ tokens: cached.tokens
8079
8368
  };
8080
8369
  }
8081
8370
  function createRolloutReader(io) {
@@ -8092,6 +8381,7 @@ function createRolloutReader(io) {
8092
8381
  lastTurnAt: tail.lastTurnAt,
8093
8382
  lastTurnImported: tail.lastTurnImported,
8094
8383
  lastLineAt: tail.lastLineAt,
8384
+ tokens: tail.tokens,
8095
8385
  ino: info.ino,
8096
8386
  size: info.size,
8097
8387
  mtimeMs: info.mtimeMs,
@@ -8133,6 +8423,8 @@ function createRolloutReader(io) {
8133
8423
  next.lastTurnAt = turn.at;
8134
8424
  next.lastTurnImported = turn.imported;
8135
8425
  }
8426
+ const counted2 = turn ? null : readTokenLine(line);
8427
+ if (counted2) next.tokens = counted2.tokens;
8136
8428
  }
8137
8429
  start = newline + 1;
8138
8430
  newline = added.indexOf(NEWLINE3, start);
@@ -8162,7 +8454,7 @@ function createRolloutReader(io) {
8162
8454
  }
8163
8455
 
8164
8456
  // src/collector/adapters/codex/rollouts.ts
8165
- import path15 from "node:path";
8457
+ import path16 from "node:path";
8166
8458
  var UUID4 = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
8167
8459
  var ROLLOUT_FILE_NAME = new RegExp(
8168
8460
  `^rollout-\\d{4}-\\d{2}-\\d{2}T\\d{2}-\\d{2}-\\d{2}-(${UUID4})(?:_(${UUID4}))?\\.jsonl$`,
@@ -8180,7 +8472,7 @@ function twoDigits(value) {
8180
8472
  function recentDayFolders(sessionsDir, now) {
8181
8473
  const folders = [now, now - 24 * 60 * 60 * 1e3].map((at) => {
8182
8474
  const date = new Date(at);
8183
- return path15.join(
8475
+ return path16.join(
8184
8476
  sessionsDir,
8185
8477
  String(date.getFullYear()),
8186
8478
  twoDigits(date.getMonth() + 1),
@@ -8209,12 +8501,12 @@ function createRolloutFinder(options) {
8209
8501
  const refs = [];
8210
8502
  for (const name of (await names(dir)).sort()) {
8211
8503
  const threadId = threadIdOfRollout(name);
8212
- if (threadId !== null) refs.push({ path: path15.join(dir, name), threadId });
8504
+ if (threadId !== null) refs.push({ path: path16.join(dir, name), threadId });
8213
8505
  }
8214
8506
  return refs;
8215
8507
  }
8216
8508
  async function subfolders(dir, pattern) {
8217
- return (await names(dir)).filter((name) => pattern.test(name)).sort().map((name) => path15.join(dir, name));
8509
+ return (await names(dir)).filter((name) => pattern.test(name)).sort().map((name) => path16.join(dir, name));
8218
8510
  }
8219
8511
  async function buildIndex() {
8220
8512
  const years = await subfolders(sessionsDir, YEAR);
@@ -8258,7 +8550,7 @@ function createRolloutFinder(options) {
8258
8550
  // src/collector/adapters/codex/sessionIndex.ts
8259
8551
  var SESSION_INDEX_FILE = "session_index.jsonl";
8260
8552
  var SESSION_INDEX_LIMIT_BYTES = 4 * 1024 * 1024;
8261
- function isRecord11(value) {
8553
+ function isRecord12(value) {
8262
8554
  return typeof value === "object" && value !== null && !Array.isArray(value);
8263
8555
  }
8264
8556
  function parseSessionIndex(content) {
@@ -8271,7 +8563,7 @@ function parseSessionIndex(content) {
8271
8563
  } catch {
8272
8564
  continue;
8273
8565
  }
8274
- if (!isRecord11(value)) continue;
8566
+ if (!isRecord12(value)) continue;
8275
8567
  const { id, thread_name: name } = value;
8276
8568
  if (typeof id !== "string" || id === "" || typeof name !== "string") continue;
8277
8569
  const trimmed = name.trim();
@@ -8360,6 +8652,7 @@ function codexSession(input) {
8360
8652
  const writtenAt = input.writtenAt === void 0 ? null : plausibleTime(Math.floor(input.writtenAt), now);
8361
8653
  const beforeStart = writtenAt !== null && startedAt !== null && writtenAt < startedAt - WRITE_ORDER_SLACK_MS3;
8362
8654
  if (writtenAt !== null && !beforeStart) session.lastWriteAt = writtenAt;
8655
+ if (state.tokens) session.tokens = { ...state.tokens };
8363
8656
  return session;
8364
8657
  }
8365
8658
 
@@ -8414,6 +8707,7 @@ var CODEX_CAPABILITIES = {
8414
8707
  reason: "Codex's files name no process to find, and Codex documents no link to a session."
8415
8708
  },
8416
8709
  "quiet-for": { level: "yes" },
8710
+ tokens: { level: "yes" },
8417
8711
  stop: {
8418
8712
  level: "no",
8419
8713
  reason: "Codex's files name no process that Agent Lookout could confirm and stop."
@@ -8433,10 +8727,10 @@ function createCodexAdapter(options = {}) {
8433
8727
  const ownOverride = env[CODEX_HOME_ENV]?.trim() || void 0;
8434
8728
  const codexOverride = env[CODEX_OWN_HOME_ENV]?.trim() || void 0;
8435
8729
  const homeFrom = ownOverride ? "agent-lookout" : codexOverride ? "codex" : "default";
8436
- const codexHome = path16.resolve(ownOverride ?? codexOverride ?? path16.join(homeDir, ".codex"));
8437
- const sessionsDir = path16.join(codexHome, "sessions");
8438
- const locksDir = path16.join(codexHome, WRITER_LOCK_DIR);
8439
- const indexFile = path16.join(codexHome, SESSION_INDEX_FILE);
8730
+ const codexHome = path17.resolve(ownOverride ?? codexOverride ?? path17.join(homeDir, ".codex"));
8731
+ const sessionsDir = path17.join(codexHome, "sessions");
8732
+ const locksDir = path17.join(codexHome, WRITER_LOCK_DIR);
8733
+ const indexFile = path17.join(codexHome, SESSION_INDEX_FILE);
8440
8734
  const homeName = tildify(codexHome, homeDir);
8441
8735
  const sessionsName = tildify(sessionsDir, homeDir);
8442
8736
  const locksName = tildify(locksDir, homeDir);
@@ -8619,8 +8913,8 @@ function createCodexAdapter(options = {}) {
8619
8913
  } else if (olderCodex) {
8620
8914
  detail += ` ${OLDER_CODEX_NOTE}`;
8621
8915
  }
8622
- const answer = result("ok", detail, every(pollIntervalMs), checkedAt);
8623
- return { ...answer, sessions, basis: locks.supported ? BASIS2 : BASIS_WITHOUT_LOCKS };
8916
+ const answer2 = result("ok", detail, every(pollIntervalMs), checkedAt);
8917
+ return { ...answer2, sessions, basis: locks.supported ? BASIS2 : BASIS_WITHOUT_LOCKS };
8624
8918
  }
8625
8919
  return {
8626
8920
  id: SOURCE_ID3,
@@ -8644,7 +8938,7 @@ function createCodexAdapter(options = {}) {
8644
8938
 
8645
8939
  // src/collector/adapters/status-files/index.ts
8646
8940
  import os7 from "node:os";
8647
- import path17 from "node:path";
8941
+ import path18 from "node:path";
8648
8942
 
8649
8943
  // src/core/mapping/statusFileMapping.ts
8650
8944
  var STATUS = /* @__PURE__ */ new Map([
@@ -8773,6 +9067,7 @@ var STATUS_FILE_CAPABILITIES = {
8773
9067
  },
8774
9068
  jump: { level: "no", reason: "Nothing in a status file is used to reach a session." },
8775
9069
  "quiet-for": { level: "partly", reason: "If the agent writes its file again as it works." },
9070
+ tokens: { level: "no", reason: "A status file has no field for token counts." },
8776
9071
  stop: {
8777
9072
  level: "no",
8778
9073
  reason: "Any program can write a status file, so nothing in one is used to stop a session."
@@ -8793,7 +9088,7 @@ function createStatusFileAdapter(options = {}) {
8793
9088
  const isAlive = options.isAlive ?? isProcessAlive;
8794
9089
  const pollIntervalMs = options.pollIntervalMs ?? POLL_INTERVAL_MS;
8795
9090
  const override = env[STATUS_DIR_ENV]?.trim() || void 0;
8796
- const dir = path17.resolve(override ?? path17.join(homeDir, ".agent-lookout", "sessions"));
9091
+ const dir = path18.resolve(override ?? path18.join(homeDir, ".agent-lookout", "sessions"));
8797
9092
  const dirName = tildify(dir, homeDir);
8798
9093
  const decoder2 = new TextDecoder();
8799
9094
  let lastRead = /* @__PURE__ */ new Map();
@@ -8853,7 +9148,7 @@ function createStatusFileAdapter(options = {}) {
8853
9148
  if (names.length <= MAX_FILES) return names;
8854
9149
  const written = await Promise.all(
8855
9150
  names.map(async (name) => {
8856
- const info = await io.lstat(path17.join(dir, name)).catch(() => null);
9151
+ const info = await io.lstat(path18.join(dir, name)).catch(() => null);
8857
9152
  return { name, mtimeMs: info?.mtimeMs ?? -Infinity };
8858
9153
  })
8859
9154
  );
@@ -8874,14 +9169,14 @@ function createStatusFileAdapter(options = {}) {
8874
9169
  names = await io.readdir(dir);
8875
9170
  } catch (error) {
8876
9171
  if (!isMissing2(error)) {
8877
- const answer = result(
9172
+ const answer2 = result(
8878
9173
  "error",
8879
9174
  `Status files could not be read: the folder ${dirName} could not be listed.`,
8880
9175
  watching("cannot be read"),
8881
9176
  checkedAt
8882
9177
  );
8883
- answer.health.advice = "Check that your user account can open the folder.";
8884
- return answer;
9178
+ answer2.health.advice = "Check that your user account can open the folder.";
9179
+ return answer2;
8885
9180
  }
8886
9181
  contentsKnown = true;
8887
9182
  lastRead = /* @__PURE__ */ new Map();
@@ -8903,7 +9198,7 @@ function createStatusFileAdapter(options = {}) {
8903
9198
  const nextRead = /* @__PURE__ */ new Map();
8904
9199
  const nextStatuses = /* @__PURE__ */ new Map();
8905
9200
  for (const fileName2 of await toRead(listed)) {
8906
- const reading = await readOne(path17.join(dir, fileName2));
9201
+ const reading = await readOne(path18.join(dir, fileName2));
8907
9202
  let kept;
8908
9203
  if (reading.kind === "read") {
8909
9204
  kept = reading;
@@ -9130,7 +9425,7 @@ function createEventStore(capacity = EVENT_CAPACITY) {
9130
9425
 
9131
9426
  // src/collector/git/branchFinder.ts
9132
9427
  import os8 from "node:os";
9133
- import path19 from "node:path";
9428
+ import path20 from "node:path";
9134
9429
 
9135
9430
  // src/collector/git/gitHead.ts
9136
9431
  var MAX_GIT_FILE_BYTES = 4 * 1024;
@@ -9170,18 +9465,18 @@ function parseCommonDir(content) {
9170
9465
 
9171
9466
  // src/collector/git/repository.ts
9172
9467
  import { createHash as createHash3 } from "node:crypto";
9173
- import path18 from "node:path";
9468
+ import path19 from "node:path";
9174
9469
  var REPOSITORY_ID_LENGTH = 16;
9175
9470
  function repositoryId(gitFolder) {
9176
9471
  return createHash3("sha256").update(gitFolder).digest("hex").slice(0, REPOSITORY_ID_LENGTH);
9177
9472
  }
9178
9473
  function sharedName(common) {
9179
- const base = path18.basename(common);
9180
- if (base.startsWith(".")) return path18.basename(path18.dirname(common));
9474
+ const base = path19.basename(common);
9475
+ if (base.startsWith(".")) return path19.basename(path19.dirname(common));
9181
9476
  return base.endsWith(".git") ? base.slice(0, -".git".length) : base;
9182
9477
  }
9183
9478
  function repositoryOf({ dir, gitDir, commonDir }) {
9184
- const name = sessionName(commonDir === null ? path18.basename(dir) : sharedName(commonDir));
9479
+ const name = sessionName(commonDir === null ? path19.basename(dir) : sharedName(commonDir));
9185
9480
  return name === void 0 ? null : { id: repositoryId(commonDir ?? gitDir), name };
9186
9481
  }
9187
9482
 
@@ -9210,13 +9505,13 @@ async function readGitFile(io, file, maxBytes = MAX_GIT_FILE_BYTES) {
9210
9505
  }
9211
9506
  }
9212
9507
  async function commonDirOf(io, gitDir) {
9213
- const content = await readGitFile(io, path19.join(gitDir, "commondir"));
9508
+ const content = await readGitFile(io, path20.join(gitDir, "commondir"));
9214
9509
  if (content === void 0) return null;
9215
9510
  const target = content === null ? null : parseCommonDir(content);
9216
- return target === null ? void 0 : path19.resolve(gitDir, target);
9511
+ return target === null ? void 0 : path20.resolve(gitDir, target);
9217
9512
  }
9218
9513
  async function headIn(dir, kind, io) {
9219
- const marker = path19.join(dir, ".git");
9514
+ const marker = path20.join(dir, ".git");
9220
9515
  let gitDir;
9221
9516
  if (kind === "directory") {
9222
9517
  gitDir = marker;
@@ -9224,11 +9519,11 @@ async function headIn(dir, kind, io) {
9224
9519
  const content2 = await readGitFile(io, marker);
9225
9520
  const target = typeof content2 === "string" ? parseGitFile(content2) : null;
9226
9521
  if (target === null) return null;
9227
- gitDir = path19.resolve(dir, target);
9522
+ gitDir = path20.resolve(dir, target);
9228
9523
  } else {
9229
9524
  return null;
9230
9525
  }
9231
- const content = await readGitFile(io, path19.join(gitDir, "HEAD"));
9526
+ const content = await readGitFile(io, path20.join(gitDir, "HEAD"));
9232
9527
  const head = typeof content === "string" ? parseHead(content) : null;
9233
9528
  if (head === null) return null;
9234
9529
  const commonDir = kind === "file" ? await commonDirOf(io, gitDir) : null;
@@ -9237,20 +9532,20 @@ async function headIn(dir, kind, io) {
9237
9532
  return { head: { ...head, repository }, gitFolder: commonDir ?? gitDir };
9238
9533
  }
9239
9534
  async function findHead(cwd, { io, homeDir }) {
9240
- if (!path19.isAbsolute(cwd)) return null;
9241
- const home = path19.resolve(homeDir);
9242
- let dir = path19.resolve(cwd);
9535
+ if (!path20.isAbsolute(cwd)) return null;
9536
+ const home = path20.resolve(homeDir);
9537
+ let dir = path20.resolve(cwd);
9243
9538
  try {
9244
9539
  if ((await io.stat(dir)).kind !== "directory") return null;
9245
9540
  } catch {
9246
9541
  return null;
9247
9542
  }
9248
9543
  for (let level = 0; level < MAX_LEVELS; level += 1) {
9249
- const parent = path19.dirname(dir);
9544
+ const parent = path20.dirname(dir);
9250
9545
  if (dir === home || dir === parent) return null;
9251
9546
  let kind;
9252
9547
  try {
9253
- kind = (await io.lstat(path19.join(dir, ".git"))).kind;
9548
+ kind = (await io.lstat(path20.join(dir, ".git"))).kind;
9254
9549
  } catch (error) {
9255
9550
  if (!isMissing2(error)) return null;
9256
9551
  dir = parent;
@@ -9333,14 +9628,14 @@ var STATUS_STATE = {
9333
9628
  PENDING: "pending",
9334
9629
  EXPECTED: "pending"
9335
9630
  };
9336
- function isRecord12(value) {
9631
+ function isRecord13(value) {
9337
9632
  return typeof value === "object" && value !== null && !Array.isArray(value);
9338
9633
  }
9339
9634
  function textOf(value) {
9340
9635
  return typeof value === "string" ? value : "";
9341
9636
  }
9342
9637
  function checkOf(value) {
9343
- if (!isRecord12(value)) return null;
9638
+ if (!isRecord13(value)) return null;
9344
9639
  const startedAt = Date.parse(textOf(value.startedAt));
9345
9640
  const at = Number.isNaN(startedAt) ? -Infinity : startedAt;
9346
9641
  if (value.__typename === "CheckRun") {
@@ -9389,7 +9684,7 @@ function readGhAnswer(stdout, repository) {
9389
9684
  } catch {
9390
9685
  return null;
9391
9686
  }
9392
- if (!isRecord12(data)) return null;
9687
+ if (!isRecord13(data)) return null;
9393
9688
  const { number } = data;
9394
9689
  const state = stateOf3(data.state, data.isDraft);
9395
9690
  const title = typeof data.title === "string" ? sessionName(data.title) : void 0;
@@ -9435,11 +9730,10 @@ function ghArguments(target) {
9435
9730
  ];
9436
9731
  }
9437
9732
  var LEFT_OUT = ["GH_FORCE_TTY", "GH_DEBUG", "DEBUG", "GH_PAGER", "PAGER"];
9438
- var OWN_SETTING = /^AGENT_LOOKOUT_/;
9439
9733
  function ghEnvironment(env) {
9440
9734
  const given = {};
9441
- for (const [name, value] of Object.entries(env)) {
9442
- if (!OWN_SETTING.test(name) && !LEFT_OUT.includes(name)) given[name] = value;
9735
+ for (const [name, value] of Object.entries(childEnvironment(env))) {
9736
+ if (!LEFT_OUT.includes(name)) given[name] = value;
9443
9737
  }
9444
9738
  return {
9445
9739
  ...given,
@@ -9528,7 +9822,7 @@ function createGhAsker(options) {
9528
9822
  }
9529
9823
 
9530
9824
  // src/collector/git/pullRequestTarget.ts
9531
- import path20 from "node:path";
9825
+ import path21 from "node:path";
9532
9826
 
9533
9827
  // src/collector/git/gitConfig.ts
9534
9828
  var MAX_CONFIG_BYTES = 64 * 1024;
@@ -9786,7 +10080,7 @@ function targetOf(config, branch, base, defaultBranch) {
9786
10080
  async function readPullRequestTarget(gitFolder, branch, io = nodeIo2) {
9787
10081
  try {
9788
10082
  if (!isAskableBranch(branch)) return null;
9789
- const text4 = await readGitFile(io, path20.join(gitFolder, "config"), MAX_CONFIG_BYTES);
10083
+ const text4 = await readGitFile(io, path21.join(gitFolder, "config"), MAX_CONFIG_BYTES);
9790
10084
  if (typeof text4 !== "string") return null;
9791
10085
  const config = parseGitConfig(text4);
9792
10086
  const base = config === null ? null : baseRemoteOf(config);
@@ -9795,7 +10089,7 @@ async function readPullRequestTarget(gitFolder, branch, io = nodeIo2) {
9795
10089
  if (base.name !== null && isSafeRemoteName(base.name)) {
9796
10090
  const head = await readGitFile(
9797
10091
  io,
9798
- path20.join(gitFolder, "refs", "remotes", base.name, "HEAD")
10092
+ path21.join(gitFolder, "refs", "remotes", base.name, "HEAD")
9799
10093
  );
9800
10094
  if (typeof head === "string") defaultBranch = parseRemoteHead(head, base.name);
9801
10095
  }
@@ -9830,19 +10124,19 @@ function createPullRequestFinder(options) {
9830
10124
  let gh = "unknown";
9831
10125
  let last = null;
9832
10126
  const due = (at, since) => at - (since ?? -Infinity) >= intervalMs;
9833
- function take(question, answer, at) {
9834
- switch (answer.kind) {
10127
+ function take(question, answer2, at) {
10128
+ switch (answer2.kind) {
9835
10129
  case "found":
9836
10130
  case "none":
9837
10131
  gh = "ready";
9838
10132
  last = { at, ok: true };
9839
10133
  answers2.set(question, {
9840
- pullRequest: answer.kind === "found" ? answer.pullRequest : null,
10134
+ pullRequest: answer2.kind === "found" ? answer2.pullRequest : null,
9841
10135
  askedAt: at
9842
10136
  });
9843
10137
  return true;
9844
10138
  case "failed":
9845
- last = { at, ok: false, reason: answer.reason };
10139
+ last = { at, ok: false, reason: answer2.reason };
9846
10140
  answers2.set(question, {
9847
10141
  pullRequest: answers2.get(question)?.pullRequest ?? null,
9848
10142
  askedAt: at
@@ -9850,7 +10144,7 @@ function createPullRequestFinder(options) {
9850
10144
  return true;
9851
10145
  case "not-found":
9852
10146
  case "signed-out":
9853
- gh = answer.kind;
10147
+ gh = answer2.kind;
9854
10148
  answers2.clear();
9855
10149
  pausedUntil = at + intervalMs;
9856
10150
  return false;
@@ -9870,14 +10164,14 @@ function createPullRequestFinder(options) {
9870
10164
  const at = now();
9871
10165
  if (at < pausedUntil) return;
9872
10166
  if (!due(at, answers2.get(question)?.askedAt)) continue;
9873
- const answer = await ask(target).catch(() => ({
10167
+ const answer2 = await ask(target).catch(() => ({
9874
10168
  kind: "failed",
9875
10169
  reason: "gh could not be run"
9876
10170
  }));
9877
10171
  if (![...reads.values()].some((read) => read.target && questionOf(read.target) === question)) {
9878
10172
  continue;
9879
10173
  }
9880
- if (!take(question, answer, now())) return;
10174
+ if (!take(question, answer2, now())) return;
9881
10175
  }
9882
10176
  }
9883
10177
  return {
@@ -9966,7 +10260,7 @@ function createClearHistoryRoute(options) {
9966
10260
  }
9967
10261
 
9968
10262
  // src/collector/history/historyKeeper.ts
9969
- import path21 from "node:path";
10263
+ import path22 from "node:path";
9970
10264
 
9971
10265
  // src/collector/history/historyFiles.ts
9972
10266
  import {
@@ -10266,7 +10560,7 @@ function createHistoryKeeper(options) {
10266
10560
  });
10267
10561
  const lock = createWriterLock({
10268
10562
  fs,
10269
- file: path21.join(dir, LOCK_FILE),
10563
+ file: path22.join(dir, LOCK_FILE),
10270
10564
  pid: options.pid,
10271
10565
  now,
10272
10566
  isAlive: options.isAlive,
@@ -10276,6 +10570,7 @@ function createHistoryKeeper(options) {
10276
10570
  const blocked = /* @__PURE__ */ new Set();
10277
10571
  const touched = /* @__PURE__ */ new Set();
10278
10572
  let queue = [];
10573
+ let writing = null;
10279
10574
  let startedAt = null;
10280
10575
  let target = null;
10281
10576
  let restored = false;
@@ -10292,7 +10587,7 @@ function createHistoryKeeper(options) {
10292
10587
  });
10293
10588
  return next;
10294
10589
  }
10295
- const full = (name) => path21.join(dir, name);
10590
+ const full = (name) => path22.join(dir, name);
10296
10591
  function hold(record) {
10297
10592
  if (unusable !== null || !running) return;
10298
10593
  const line = encodeRecord(record);
@@ -10468,7 +10763,7 @@ function createHistoryKeeper(options) {
10468
10763
  bytes3 += lineBytes;
10469
10764
  end += 1;
10470
10765
  }
10471
- const outcome = yield { name, text: text4 };
10766
+ const outcome = yield { name, text: text4, count: end - index };
10472
10767
  if (outcome === "blocked") {
10473
10768
  blocked.add(name);
10474
10769
  part += 1;
@@ -10498,11 +10793,15 @@ function createHistoryKeeper(options) {
10498
10793
  }
10499
10794
  async function writeLines(lines) {
10500
10795
  if (lines.length === 0) return true;
10796
+ const job = { lines: [...lines] };
10797
+ writing = job;
10501
10798
  const steps = chunks(lines);
10502
10799
  let step = steps.next("written");
10503
10800
  while (!step.done) {
10504
- const { name, text: text4 } = step.value;
10801
+ if (writing !== job) return false;
10802
+ const { name, text: text4, count: count2 } = step.value;
10505
10803
  await prune(name, byteLength(text4));
10804
+ if (writing !== job) return false;
10506
10805
  try {
10507
10806
  await fs.append(full(name), text4);
10508
10807
  } catch (error) {
@@ -10510,11 +10809,14 @@ function createHistoryKeeper(options) {
10510
10809
  step = steps.next("blocked");
10511
10810
  continue;
10512
10811
  }
10812
+ writing = null;
10513
10813
  writeFailed();
10514
10814
  return false;
10515
10815
  }
10816
+ job.lines = job.lines.slice(count2);
10516
10817
  step = steps.next("written");
10517
10818
  }
10819
+ writing = null;
10518
10820
  wrote();
10519
10821
  return true;
10520
10822
  }
@@ -10643,8 +10945,10 @@ function createHistoryKeeper(options) {
10643
10945
  running = false;
10644
10946
  if (timer) clearInterval(timer);
10645
10947
  timer = null;
10948
+ const left = [...writing?.lines ?? [], ...takeQueued()];
10949
+ writing = null;
10646
10950
  if (writer === true && unusable === null && lock.stillHeldNow()) {
10647
- writeLinesNow(takeQueued());
10951
+ writeLinesNow(left);
10648
10952
  }
10649
10953
  queue = [];
10650
10954
  startedAt = null;
@@ -10754,13 +11058,13 @@ function keptHistoryStore(store, keeper) {
10754
11058
  // src/collector/history/historySettings.ts
10755
11059
  import { createHash as createHash4 } from "node:crypto";
10756
11060
  import os9 from "node:os";
10757
- import path22 from "node:path";
11061
+ import path23 from "node:path";
10758
11062
  var HISTORY_ENV = "AGENT_LOOKOUT_HISTORY";
10759
11063
  var HISTORY_DIR_ENV = "AGENT_LOOKOUT_HISTORY_DIR";
10760
11064
  function readHistorySetup(env, homeDir = os9.homedir()) {
10761
11065
  if (env[HISTORY_ENV]?.trim().toLowerCase() === "off") return { on: false };
10762
11066
  const override = env[HISTORY_DIR_ENV]?.trim() || void 0;
10763
- const dir = path22.resolve(override ?? path22.join(homeDir, ".agent-lookout", "history"));
11067
+ const dir = path23.resolve(override ?? path23.join(homeDir, ".agent-lookout", "history"));
10764
11068
  return {
10765
11069
  on: true,
10766
11070
  dir,
@@ -10771,14 +11075,57 @@ function readHistorySetup(env, homeDir = os9.homedir()) {
10771
11075
  function sourcesFingerprint(env, homeDir = os9.homedir()) {
10772
11076
  const set = (name) => env[name]?.trim() || void 0;
10773
11077
  const folders = [
10774
- path22.resolve(set(CLAUDE_HOME_ENV) ?? path22.join(homeDir, ".claude")),
10775
- path22.resolve(set(CODEX_HOME_ENV) ?? set(CODEX_OWN_HOME_ENV) ?? path22.join(homeDir, ".codex")),
10776
- path22.resolve(set(STATUS_DIR_ENV) ?? path22.join(homeDir, ".agent-lookout", "sessions")),
10777
- path22.resolve(set(ANTIGRAVITY_HOME_ENV) ?? path22.join(homeDir, ...DEFAULT_HOME))
11078
+ path23.resolve(set(CLAUDE_HOME_ENV) ?? path23.join(homeDir, ".claude")),
11079
+ path23.resolve(set(CODEX_HOME_ENV) ?? set(CODEX_OWN_HOME_ENV) ?? path23.join(homeDir, ".codex")),
11080
+ path23.resolve(set(STATUS_DIR_ENV) ?? path23.join(homeDir, ".agent-lookout", "sessions")),
11081
+ path23.resolve(set(ANTIGRAVITY_HOME_ENV) ?? path23.join(homeDir, ...DEFAULT_HOME))
10778
11082
  ];
10779
11083
  return createHash4("sha256").update(JSON.stringify(folders)).digest("hex").slice(0, 32);
10780
11084
  }
10781
11085
 
11086
+ // src/collector/messages/lastMessageRoute.ts
11087
+ var answer = (body) => ({ status: 200, body });
11088
+ var failed3 = (status, error, headers) => ({
11089
+ status,
11090
+ body: { error },
11091
+ ...headers && { headers }
11092
+ });
11093
+ function idIn(query) {
11094
+ const pairs = [...query];
11095
+ if (pairs.length !== 1) return null;
11096
+ const [name, value] = pairs[0] ?? ["", ""];
11097
+ if (name !== "id" || value === "" || value.length > MAX_SESSION_ID_LENGTH) return null;
11098
+ return value;
11099
+ }
11100
+ function createLastMessageRoute(options) {
11101
+ const { poller, adapters } = options;
11102
+ const off = lastMessageOff(options.env);
11103
+ return async (req, url) => {
11104
+ const site = req.headers["sec-fetch-site"];
11105
+ if (site !== void 0 && site !== "same-origin") {
11106
+ return failed3(403, "This address only answers the dashboard page served from this machine.");
11107
+ }
11108
+ const id = idIn(url.searchParams);
11109
+ if (id === null) {
11110
+ return failed3(400, "Name one session as id, and nothing else.");
11111
+ }
11112
+ if (off) return answer({ message: null, reason: "off", setting: LAST_MESSAGE_ENV });
11113
+ const session = poller.getSnapshot().sessions.find((listed) => listed.id === id);
11114
+ const notListed = () => failed3(404, "No session with that id is listed.");
11115
+ if (session === void 0) return notListed();
11116
+ const adapter = adapters.find((candidate) => candidate.id === session.source);
11117
+ if (adapter?.lastMessage === void 0) return answer({ message: null, reason: "not-read" });
11118
+ const read = await adapter.lastMessage(session.id);
11119
+ if (read === null) return notListed();
11120
+ if (read === "busy") {
11121
+ return failed3(429, "Too many last messages were asked for at once. Ask again in a second.", {
11122
+ "Retry-After": "1"
11123
+ });
11124
+ }
11125
+ return answer(read);
11126
+ };
11127
+ }
11128
+
10782
11129
  // src/collector/notifications/heldWait.ts
10783
11130
  var HANDOVER_GRACE_MS = 3e3;
10784
11131
  var PAGE_GONE_AFTER_MS = 5e3;
@@ -10944,6 +11291,7 @@ function runOsascript(args, options = {}) {
10944
11291
  OSASCRIPT,
10945
11292
  [...args],
10946
11293
  {
11294
+ env: childEnvironment(process.env),
10947
11295
  timeout: options.timeoutMs ?? TERMINAL_JUMP_TIMEOUT_MS,
10948
11296
  maxBuffer: MAX_OUTPUT_BYTES4,
10949
11297
  encoding: "utf8",
@@ -11266,7 +11614,7 @@ function channelIn(body) {
11266
11614
  function asSentence(reason) {
11267
11615
  return `${reason.charAt(0).toUpperCase()}${reason.slice(1)}.`;
11268
11616
  }
11269
- var failed3 = (status, reason, error, more = {}) => ({
11617
+ var failed4 = (status, reason, error, more = {}) => ({
11270
11618
  status,
11271
11619
  body: {
11272
11620
  error,
@@ -11298,21 +11646,21 @@ function createTestSendRoute(options) {
11298
11646
  const { name, setting } = NAMED[channel];
11299
11647
  const tester = channels[channel];
11300
11648
  if (tester === null) {
11301
- return failed3(
11649
+ return failed4(
11302
11650
  409,
11303
11651
  "off",
11304
11652
  `${name} is not set up. Set ${setting} and start Agent Lookout again.`
11305
11653
  );
11306
11654
  }
11307
11655
  if (!limiter.begin()) {
11308
- return failed3(429, "too-soon", "One test is sent a second. Try again in a moment.", {
11656
+ return failed4(429, "too-soon", "One test is sent a second. Try again in a moment.", {
11309
11657
  retryAfter: 1
11310
11658
  });
11311
11659
  }
11312
11660
  try {
11313
11661
  const result = await tester.test();
11314
11662
  if (!result.tried) {
11315
- return failed3(
11663
+ return failed4(
11316
11664
  429,
11317
11665
  "limited",
11318
11666
  `${SENDS_PER_HOUR} ${name} pushes were tried in the last hour, the most it tries.`,
@@ -11323,7 +11671,7 @@ function createTestSendRoute(options) {
11323
11671
  );
11324
11672
  }
11325
11673
  if (!result.outcome.sent) {
11326
- return failed3(502, "not-sent", asSentence(result.outcome.reason));
11674
+ return failed4(502, "not-sent", asSentence(result.outcome.reason));
11327
11675
  }
11328
11676
  return {
11329
11677
  status: 200,
@@ -11423,12 +11771,12 @@ function askRemote(port, route, timeoutMs = REMOTE_ANSWER_TIMEOUT_MS) {
11423
11771
  return new Promise((resolve) => {
11424
11772
  let connected = false;
11425
11773
  let settled = false;
11426
- const finish = (answer) => {
11774
+ const finish = (answer2) => {
11427
11775
  if (settled) return;
11428
11776
  settled = true;
11429
11777
  clearTimeout(timer);
11430
11778
  req.destroy();
11431
- resolve(answer);
11779
+ resolve(answer2);
11432
11780
  };
11433
11781
  const req = request({
11434
11782
  host: "127.0.0.1",
@@ -11491,7 +11839,7 @@ var MAX_REMOTE_SOURCES = 20;
11491
11839
  var MAX_ID_LENGTH = 512;
11492
11840
  var MAX_PATH_LENGTH = 4096;
11493
11841
  var MAX_WORD_LENGTH2 = 200;
11494
- function isRecord13(value) {
11842
+ function isRecord14(value) {
11495
11843
  return typeof value === "object" && value !== null && !Array.isArray(value);
11496
11844
  }
11497
11845
  function oneOf2(allowed, value) {
@@ -11505,7 +11853,7 @@ function exact(value, max) {
11505
11853
  var COMMIT_ID2 = /^[0-9a-f]{7,64}$/;
11506
11854
  var REPOSITORY_ID = /^[0-9a-f]{16,64}$/;
11507
11855
  function readGit(value, machine) {
11508
- if (!isRecord13(value)) return void 0;
11856
+ if (!isRecord14(value)) return void 0;
11509
11857
  let head;
11510
11858
  if (value.commit !== void 0) {
11511
11859
  if (value.branch !== void 0 || typeof value.commit !== "string") return void 0;
@@ -11517,7 +11865,7 @@ function readGit(value, machine) {
11517
11865
  head = { branch };
11518
11866
  }
11519
11867
  const repository = value.repository;
11520
- if (isRecord13(repository) && typeof repository.id === "string") {
11868
+ if (isRecord14(repository) && typeof repository.id === "string") {
11521
11869
  const name = sessionName(typeof repository.name === "string" ? repository.name : "");
11522
11870
  if (name !== void 0 && REPOSITORY_ID.test(repository.id)) {
11523
11871
  head.repository = { id: repositoryId(`${machine}
@@ -11527,7 +11875,7 @@ ${repository.id}`), name };
11527
11875
  return head;
11528
11876
  }
11529
11877
  function readSession(value, machine, sources, now, keepWaitingText) {
11530
- if (!isRecord13(value)) return null;
11878
+ if (!isRecord14(value)) return null;
11531
11879
  const id = exact(value.id, MAX_ID_LENGTH);
11532
11880
  const source = exact(value.source, MAX_WORD_LENGTH2);
11533
11881
  if (id === null || source === null) return null;
@@ -11561,13 +11909,15 @@ function readSession(value, machine, sources, now, keepWaitingText) {
11561
11909
  }
11562
11910
  const lastWriteAt = plausibleTime(value.lastWriteAt, now);
11563
11911
  if (lastWriteAt !== null) session.lastWriteAt = lastWriteAt;
11912
+ const tokens = tokenCountsOf(value.tokens);
11913
+ if (tokens) session.tokens = tokens;
11564
11914
  if (typeof value.alive === "boolean") session.alive = value.alive;
11565
11915
  const git = readGit(value.git, machine);
11566
11916
  if (git) session.git = git;
11567
11917
  return session;
11568
11918
  }
11569
11919
  function readCell(value) {
11570
- if (!isRecord13(value)) return null;
11920
+ if (!isRecord14(value)) return null;
11571
11921
  if (value.level === "yes") return { level: "yes" };
11572
11922
  const level = oneOf2(["no", "partly"], value.level);
11573
11923
  const reason = cut(value.reason, MAX_WORD_LENGTH2);
@@ -11575,10 +11925,17 @@ function readCell(value) {
11575
11925
  }
11576
11926
  var ACTS_HERE = ["jump", "stop", "answer"];
11577
11927
  function readCapabilities(value, machine) {
11578
- if (!isRecord13(value)) return null;
11928
+ if (!isRecord14(value)) return null;
11579
11929
  const read = {};
11580
11930
  for (const capability of CAPABILITIES) {
11581
11931
  if (ACTS_HERE.includes(capability)) continue;
11932
+ if (capability === "tokens" && value.tokens === void 0) {
11933
+ read.tokens = {
11934
+ level: "no",
11935
+ reason: `The Agent Lookout on ${machine} does not send token counts.`
11936
+ };
11937
+ continue;
11938
+ }
11582
11939
  const cell = readCell(value[capability]);
11583
11940
  if (!cell) return null;
11584
11941
  read[capability] = cell;
@@ -11591,7 +11948,7 @@ function readCapabilities(value, machine) {
11591
11948
  };
11592
11949
  }
11593
11950
  function readRemoteSnapshot(value, machine, now, options = {}) {
11594
- if (!isRecord13(value) || !Array.isArray(value.sessions) || !Array.isArray(value.sources)) {
11951
+ if (!isRecord14(value) || !Array.isArray(value.sessions) || !Array.isArray(value.sources)) {
11595
11952
  return null;
11596
11953
  }
11597
11954
  const named = /* @__PURE__ */ new Map();
@@ -11599,7 +11956,7 @@ function readRemoteSnapshot(value, machine, now, options = {}) {
11599
11956
  const agents = [];
11600
11957
  for (const item of value.sources) {
11601
11958
  if (sources.length >= MAX_REMOTE_SOURCES) break;
11602
- if (!isRecord13(item)) continue;
11959
+ if (!isRecord14(item)) continue;
11603
11960
  const id = exact(item.id, MAX_WORD_LENGTH2);
11604
11961
  if (id === null || isRemoteSource(id) || item.machine !== void 0) continue;
11605
11962
  const label2 = cut(item.label, MAX_WORD_LENGTH2) ?? id;
@@ -11624,7 +11981,7 @@ function readRemoteSnapshot(value, machine, now, options = {}) {
11624
11981
  }
11625
11982
 
11626
11983
  // src/collector/remotes/sshProgram.ts
11627
- import path23 from "node:path";
11984
+ import path24 from "node:path";
11628
11985
  var SSH_BIN_ENV = "AGENT_LOOKOUT_SSH_BIN";
11629
11986
  var SSH_LOCATIONS = ["/usr/bin/ssh", "/opt/homebrew/bin/ssh", "/usr/local/bin/ssh"];
11630
11987
  var SERVER_ALIVE_SECONDS = 15;
@@ -11646,7 +12003,7 @@ async function findSsh(env, isExecutable, platform = process.platform) {
11646
12003
  }
11647
12004
  return {
11648
12005
  found: false,
11649
- looked: `The ssh command was not found on PATH or in ${SSH_LOCATIONS.map((at) => path23.dirname(at)).join(", ")}`
12006
+ looked: `The ssh command was not found on PATH or in ${SSH_LOCATIONS.map((at) => path24.dirname(at)).join(", ")}`
11650
12007
  };
11651
12008
  }
11652
12009
  function sshArguments(forward) {
@@ -11988,7 +12345,11 @@ function freeLoopbackPort() {
11988
12345
  });
11989
12346
  });
11990
12347
  }
11991
- var spawnSsh = (binary, args, env) => spawn2(binary, [...args], { env, stdio: ["ignore", "ignore", "pipe"], windowsHide: true });
12348
+ var spawnSsh = (binary, args, env) => spawn2(binary, [...args], {
12349
+ env: childEnvironment(env),
12350
+ stdio: ["ignore", "ignore", "pipe"],
12351
+ windowsHide: true
12352
+ });
11992
12353
  var live = /* @__PURE__ */ new Set();
11993
12354
  var endedOnExit = false;
11994
12355
  function endOnExit(child) {
@@ -12242,14 +12603,14 @@ import {
12242
12603
  writeSync as writeSync2
12243
12604
  } from "node:fs";
12244
12605
  import os10 from "node:os";
12245
- import path24 from "node:path";
12606
+ import path25 from "node:path";
12246
12607
  var SETTINGS_FILE_ENV = "AGENT_LOOKOUT_SETTINGS_FILE";
12247
12608
  var SETTINGS_FILE_MODE = 384;
12248
12609
  var SETTINGS_FOLDER_MODE = 448;
12249
12610
  var MAX_SETTINGS_BYTES = 64 * 1024;
12250
12611
  function readSettingsSetup(env, homeDir = os10.homedir()) {
12251
12612
  const named = env[SETTINGS_FILE_ENV]?.trim() || void 0;
12252
- const file = path24.resolve(named ?? path24.join(homeDir, ".agent-lookout", "settings.json"));
12613
+ const file = path25.resolve(named ?? path25.join(homeDir, ".agent-lookout", "settings.json"));
12253
12614
  return { file, shown: tildify(file, homeDir) };
12254
12615
  }
12255
12616
  function codeOf(error) {
@@ -12294,7 +12655,7 @@ function readSettingsStamp(setup) {
12294
12655
  }
12295
12656
  function readSettingsText(setup) {
12296
12657
  const { file, shown } = setup;
12297
- const folder = path24.dirname(file);
12658
+ const folder = path25.dirname(file);
12298
12659
  try {
12299
12660
  const folderKind = kindAt(folder);
12300
12661
  if (folderKind === null) return { kind: "missing" };
@@ -12353,7 +12714,7 @@ function readSettingsText(setup) {
12353
12714
  }
12354
12715
  function writeSettingsText(setup, text4) {
12355
12716
  const { file, shown } = setup;
12356
- const folder = path24.dirname(file);
12717
+ const folder = path25.dirname(file);
12357
12718
  const notSaved = {
12358
12719
  ok: false,
12359
12720
  problem: `${shown} could not be written, so the change was not saved.`
@@ -12374,9 +12735,9 @@ function writeSettingsText(setup, text4) {
12374
12735
  problem: `${shown} is a link or not an ordinary file, which Agent Lookout does not write through, so the change was not saved.`
12375
12736
  };
12376
12737
  }
12377
- temporary = path24.join(
12738
+ temporary = path25.join(
12378
12739
  folder,
12379
- `.${path24.basename(file)}.${process.pid}.${randomBytes3(6).toString("hex")}.tmp`
12740
+ `.${path25.basename(file)}.${process.pid}.${randomBytes3(6).toString("hex")}.tmp`
12380
12741
  );
12381
12742
  const fd = openWithoutFollowingNow(
12382
12743
  temporary,
@@ -12416,7 +12777,7 @@ var RULE_WORDS = {
12416
12777
  idle: "the idle rule",
12417
12778
  quietHours: "the quiet hours"
12418
12779
  };
12419
- function isRecord14(value) {
12780
+ function isRecord15(value) {
12420
12781
  return typeof value === "object" && value !== null && !Array.isArray(value);
12421
12782
  }
12422
12783
  function listOf(names) {
@@ -12454,7 +12815,7 @@ function rulesIn(read, shown) {
12454
12815
  } catch {
12455
12816
  value = null;
12456
12817
  }
12457
- if (!isRecord14(value)) {
12818
+ if (!isRecord15(value)) {
12458
12819
  const notJson = `${shown} is not JSON that Agent Lookout can read, so the time rules and the permission rules are off. Changing one in Settings writes the file again.`;
12459
12820
  return {
12460
12821
  rules: off,
@@ -12469,7 +12830,7 @@ function rulesIn(read, shown) {
12469
12830
  const { timeRules, permissionRules: heldRules, ...rest } = value;
12470
12831
  const permissions = readPermissionRules(heldRules);
12471
12832
  const { rules, unread } = readTimeRules(timeRules);
12472
- const otherRules = isRecord14(timeRules) ? Object.fromEntries(
12833
+ const otherRules = isRecord15(timeRules) ? Object.fromEntries(
12473
12834
  Object.entries(timeRules).filter(
12474
12835
  ([name]) => !TIME_RULE_NAMES.includes(name)
12475
12836
  )
@@ -12571,15 +12932,15 @@ import { randomBytes as randomBytes4 } from "node:crypto";
12571
12932
 
12572
12933
  // src/core/permission-rules/rulesChange.ts
12573
12934
  var CHANGE_SHAPE = 'The body must be one change: {"add": rule}, {"edit": rule with its "id"}, {"move": {"id", "to": "up" or "down"}} or {"remove": {"id"}}, and nothing else.';
12574
- function isRecord15(value) {
12935
+ function isRecord16(value) {
12575
12936
  return typeof value === "object" && value !== null && !Array.isArray(value);
12576
12937
  }
12577
12938
  function rulesChangeIn(value) {
12578
12939
  const refused = { ok: false, problem: CHANGE_SHAPE };
12579
- if (!isRecord15(value) || Object.keys(value).length !== 1) return refused;
12940
+ if (!isRecord16(value) || Object.keys(value).length !== 1) return refused;
12580
12941
  const [kind] = Object.keys(value);
12581
12942
  const body = value[kind];
12582
- if (!isRecord15(body)) return refused;
12943
+ if (!isRecord16(body)) return refused;
12583
12944
  switch (kind) {
12584
12945
  case "add": {
12585
12946
  const read = ruleWordsIn(body);
@@ -12655,7 +13016,7 @@ function applyRulesChange(rules, change, newId) {
12655
13016
 
12656
13017
  // src/collector/settings/permissionRulesRoute.ts
12657
13018
  var MAX_PERMISSION_RULES_BODY_BYTES = 1024;
12658
- var failed4 = (status, reason, error) => ({
13019
+ var failed5 = (status, reason, error) => ({
12659
13020
  status,
12660
13021
  body: { error, reason }
12661
13022
  });
@@ -12682,14 +13043,14 @@ function createPermissionRulesRoute(options) {
12682
13043
  try {
12683
13044
  value = JSON.parse(body.text);
12684
13045
  } catch {
12685
- return failed4(400, "invalid", "The body must be JSON: one change to the permission rules.");
13046
+ return failed5(400, "invalid", "The body must be JSON: one change to the permission rules.");
12686
13047
  }
12687
13048
  const asked = rulesChangeIn(value);
12688
- if (!asked.ok) return failed4(400, "invalid", asked.problem);
13049
+ if (!asked.ok) return failed5(400, "invalid", asked.problem);
12689
13050
  const made = settings.changePermissionRules(
12690
13051
  (rules) => applyRulesChange(rules, asked.change, newId)
12691
13052
  );
12692
- if (!made.ok) return failed4(STATUS_OF[made.reason], made.reason, made.problem);
13053
+ if (!made.ok) return failed5(STATUS_OF[made.reason], made.reason, made.problem);
12693
13054
  return {
12694
13055
  status: 200,
12695
13056
  body: { ok: true, permissionRules: made.rules }
@@ -12699,7 +13060,7 @@ function createPermissionRulesRoute(options) {
12699
13060
 
12700
13061
  // src/collector/settings/timeRulesRoute.ts
12701
13062
  var MAX_TIME_RULES_BODY_BYTES = 2048;
12702
- var failed5 = (status, reason, error) => ({
13063
+ var failed6 = (status, reason, error) => ({
12703
13064
  status,
12704
13065
  body: { error, reason }
12705
13066
  });
@@ -12723,12 +13084,12 @@ function createTimeRulesRoute(options) {
12723
13084
  try {
12724
13085
  value = JSON.parse(body.text);
12725
13086
  } catch {
12726
- return failed5(400, "invalid", "The body must be JSON: the three time rules.");
13087
+ return failed6(400, "invalid", "The body must be JSON: the three time rules.");
12727
13088
  }
12728
13089
  const asked = timeRulesIn(value);
12729
- if (!asked.ok) return failed5(400, "invalid", asked.problem);
13090
+ if (!asked.ok) return failed6(400, "invalid", asked.problem);
12730
13091
  const saved = settings.changeTimeRules(asked.rules);
12731
- if (!saved.ok) return failed5(500, "not-saved", saved.problem);
13092
+ if (!saved.ok) return failed6(500, "not-saved", saved.problem);
12732
13093
  return {
12733
13094
  status: 200,
12734
13095
  body: { ok: true, timeRules: asked.rules }
@@ -12751,8 +13112,8 @@ function parseProcessTable(stdout) {
12751
13112
  }
12752
13113
  var readProcessTableWithPs = async () => {
12753
13114
  if (process.platform !== "darwin") return /* @__PURE__ */ new Map();
12754
- const answer = await runPs(PS_TABLE_ARGS);
12755
- return answer.ok ? parseProcessTable(answer.stdout) : /* @__PURE__ */ new Map();
13115
+ const answer2 = await runPs(PS_TABLE_ARGS);
13116
+ return answer2.ok ? parseProcessTable(answer2.stdout) : /* @__PURE__ */ new Map();
12756
13117
  };
12757
13118
  function parentsIn(table) {
12758
13119
  const parents = /* @__PURE__ */ new Map();
@@ -12896,7 +13257,7 @@ function runTmuxBinary(binary, args, options) {
12896
13257
  binary,
12897
13258
  [...args],
12898
13259
  {
12899
- env: options.env,
13260
+ env: childEnvironment(options.env),
12900
13261
  timeout: options.timeoutMs ?? TMUX_TIMEOUT_MS,
12901
13262
  maxBuffer: MAX_OUTPUT_BYTES5,
12902
13263
  encoding: "utf8",
@@ -13304,6 +13665,10 @@ function createCollector(options) {
13304
13665
  }),
13305
13666
  pullRequests: () => pullRequests?.status() ?? pullRequestsOff,
13306
13667
  jump: createJumpRoute({ poller, panes, run: tmux, tabs, osascript, now }),
13668
+ // Only this computer's own adapters read what a session last said. A
13669
+ // session on another machine is answered as not read, and nothing is
13670
+ // asked of that machine.
13671
+ lastMessage: createLastMessageRoute({ env, poller, adapters: localAdapters }),
13307
13672
  stop: stopRoutes && createStopRoute(stopRoutes),
13308
13673
  cleanUp: stopRoutes && createCleanUpRoute(stopRoutes),
13309
13674
  settings: () => ({
@@ -13433,7 +13798,7 @@ async function runStandalone(options) {
13433
13798
  } catch (error) {
13434
13799
  return stopWith(error.message);
13435
13800
  }
13436
- if (!existsSync(path25.join(distDir, "index.html"))) {
13801
+ if (!existsSync(path26.join(distDir, "index.html"))) {
13437
13802
  return stopWith(
13438
13803
  "The dashboard has not been built yet. Run `npm run build`, then start it again."
13439
13804
  );
@@ -13476,12 +13841,12 @@ async function runStandalone(options) {
13476
13841
 
13477
13842
  // src/cli/start/openBrowser.ts
13478
13843
  import { spawn as spawn3 } from "node:child_process";
13479
- import path26 from "node:path";
13844
+ import path27 from "node:path";
13480
13845
  var OPEN_WAIT_MS = 1e4;
13481
13846
  var WINDOWS_FOLDER = "C:\\Windows";
13482
13847
  function windowsFolder(env) {
13483
13848
  const named = env.SystemRoot?.trim() || env.SYSTEMROOT?.trim();
13484
- return named && path26.win32.isAbsolute(named) ? named : WINDOWS_FOLDER;
13849
+ return named && path27.win32.isAbsolute(named) ? named : WINDOWS_FOLDER;
13485
13850
  }
13486
13851
  function openCommand(platform, url, env = process.env) {
13487
13852
  const address = loopbackAddress(url);
@@ -13490,7 +13855,7 @@ function openCommand(platform, url, env = process.env) {
13490
13855
  if (platform === "linux") return { file: "xdg-open", args: [address] };
13491
13856
  if (platform === "win32") {
13492
13857
  return {
13493
- file: path26.win32.join(windowsFolder(env), "System32", "rundll32.exe"),
13858
+ file: path27.win32.join(windowsFolder(env), "System32", "rundll32.exe"),
13494
13859
  args: ["url.dll,FileProtocolHandler", address]
13495
13860
  };
13496
13861
  }
@@ -13504,7 +13869,12 @@ function runProgram2(file, args, waitMs = OPEN_WAIT_MS) {
13504
13869
  resolve(opened);
13505
13870
  };
13506
13871
  try {
13507
- const child = spawn3(file, [...args], { stdio: "ignore", detached: true, windowsHide: true });
13872
+ const child = spawn3(file, [...args], {
13873
+ env: childEnvironment(process.env),
13874
+ stdio: "ignore",
13875
+ detached: true,
13876
+ windowsHide: true
13877
+ });
13508
13878
  child.once("error", () => settle(false));
13509
13879
  child.once("exit", (code) => settle(code === 0));
13510
13880
  child.unref();