agent-lookout 0.2.8 → 0.2.9

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-CMDNRNYP.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-XV7BYWVY.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";
@@ -4128,6 +4350,8 @@ function createClaudeCodeAdapter(options = {}) {
4128
4350
  const homeName = tildify(claudeHome, homeDir, platform);
4129
4351
  const registryName = tildify(sessionsDir, homeDir, platform);
4130
4352
  const waitingTexts = transcriptsOff ? null : createWaitingTextReader({ claudeHome, io: options.transcriptIo, now });
4353
+ const lastMessagesOff = lastMessageOff(env);
4354
+ const lastMessages = transcriptsOff || lastMessagesOff ? null : createLastMessageReader({ claudeHome, io: options.transcriptIo, now });
4131
4355
  let lastDue = null;
4132
4356
  let lastRead = null;
4133
4357
  let feedSoon = false;
@@ -4149,7 +4373,7 @@ function createClaudeCodeAdapter(options = {}) {
4149
4373
  { label: "Command run", value: commandRun },
4150
4374
  {
4151
4375
  label: "Transcript read",
4152
- value: transcriptsOff ? "Off" : "last message of a waiting session"
4376
+ value: transcriptsOff ? "Off" : lastMessagesOff ? "last message of a waiting session" : "last message of a waiting session, and of one whose details are open"
4153
4377
  }
4154
4378
  ];
4155
4379
  }
@@ -4245,12 +4469,12 @@ function createClaudeCodeAdapter(options = {}) {
4245
4469
  }
4246
4470
  reading ??= await readRegistryNow(checkedAt);
4247
4471
  const read = lastRead;
4248
- const answer = read?.feed.ok ? read.feed.entries : null;
4472
+ const answer2 = read?.feed.ok ? read.feed.entries : null;
4249
4473
  const problem = problemWith(reading, read);
4250
4474
  const { registry, live: live2 } = reading;
4251
4475
  const offered = (result) => {
4252
4476
  if (!stops) return result;
4253
- const found = withStopOffers(result.sessions, live2, answer, {
4477
+ const found = withStopOffers(result.sessions, live2, answer2, {
4254
4478
  sessionsDir,
4255
4479
  background: !neverRun
4256
4480
  });
@@ -4259,7 +4483,7 @@ function createClaudeCodeAdapter(options = {}) {
4259
4483
  };
4260
4484
  if (options.panes || options.terminals) {
4261
4485
  const pids = new Set(live2.map((entry) => entry.pid));
4262
- for (const entry of answer ?? []) {
4486
+ for (const entry of answer2 ?? []) {
4263
4487
  if (entry.pid !== void 0 && isAlive(entry.pid)) pids.add(entry.pid);
4264
4488
  }
4265
4489
  await Promise.all([options.panes?.look([...pids]), options.terminals?.look([...pids])]);
@@ -4279,16 +4503,16 @@ function createClaudeCodeAdapter(options = {}) {
4279
4503
  ...advice2 !== void 0 && { advice: advice2 },
4280
4504
  checkedAt
4281
4505
  });
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));
4506
+ if (answer2 !== null && read !== null && problem === null) {
4507
+ const listedPids = new Set(answer2.map((entry) => entry.pid));
4508
+ const listedSessionIds = new Set(answer2.map((entry) => entry.sessionId));
4285
4509
  const tooNewSince = read.askedAt - NEW_SESSION_GRACE_MS;
4286
4510
  const fromRegistry = live2.filter(
4287
4511
  (entry) => listedPids.has(entry.pid) || listedSessionIds.has(entry.sessionId) || startedAfter(entry, tooNewSince, checkedAt)
4288
4512
  );
4289
4513
  const livePids = new Set(live2.map((entry) => entry.pid));
4290
4514
  const liveSessionIds = new Set(live2.map((entry) => entry.sessionId));
4291
- const jobs = answer.filter(
4515
+ const jobs = answer2.filter(
4292
4516
  (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
4517
  ).map((entry) => {
4294
4518
  const gone = entry.pid !== void 0 && !isAlive(entry.pid);
@@ -4309,9 +4533,9 @@ function createClaudeCodeAdapter(options = {}) {
4309
4533
  basis: "registry+feed"
4310
4534
  });
4311
4535
  }
4312
- if (answer !== null && problem !== null) {
4536
+ if (answer2 !== null && problem !== null) {
4313
4537
  const entries = registry.readable ? registry.entries : void 0;
4314
- const sessions = answer.map(
4538
+ const sessions = answer2.map(
4315
4539
  (entry) => sessionFromFeed(
4316
4540
  entry,
4317
4541
  entry.pid !== void 0 ? entries?.get(entry.pid) : void 0,
@@ -4365,9 +4589,12 @@ function createClaudeCodeAdapter(options = {}) {
4365
4589
  lookingIn: neverRun ? `Looking for sessions in ${registryName}.` : `Looking for sessions in ${registryName} and with claude ${FEED_ARGS.join(" ")}.`,
4366
4590
  async poll() {
4367
4591
  try {
4368
- return await withWaitingText(await poll());
4592
+ const result = await withWaitingText(await poll());
4593
+ lastMessages?.keep(result.sessions);
4594
+ return result;
4369
4595
  } catch {
4370
4596
  await waitingTexts?.annotate([]);
4597
+ lastMessages?.forget();
4371
4598
  stops?.set(/* @__PURE__ */ new Map());
4372
4599
  return {
4373
4600
  health: {
@@ -4385,6 +4612,19 @@ function createClaudeCodeAdapter(options = {}) {
4385
4612
  sessions: []
4386
4613
  };
4387
4614
  }
4615
+ },
4616
+ async lastMessage(sessionId) {
4617
+ if (lastMessages === null) {
4618
+ return {
4619
+ message: null,
4620
+ reason: "off",
4621
+ setting: transcriptsOff ? WAITING_TEXT_ENV : LAST_MESSAGE_ENV
4622
+ };
4623
+ }
4624
+ return lastMessages.read(sessionId);
4625
+ },
4626
+ get lastMessagesKept() {
4627
+ return lastMessages?.size ?? 0;
4388
4628
  }
4389
4629
  };
4390
4630
  }
@@ -4400,8 +4640,8 @@ function parseProcessParents(stdout) {
4400
4640
  return parents;
4401
4641
  }
4402
4642
  var readProcessParentsWithPs = async () => {
4403
- const answer = await runPs(PS_PARENTS_ARGS);
4404
- return answer.ok ? parseProcessParents(answer.stdout) : /* @__PURE__ */ new Map();
4643
+ const answer2 = await runPs(PS_PARENTS_ARGS);
4644
+ return answer2.ok ? parseProcessParents(answer2.stdout) : /* @__PURE__ */ new Map();
4405
4645
  };
4406
4646
 
4407
4647
  // src/collector/actions/stopSession.ts
@@ -4869,7 +5109,7 @@ function createAnswerer(options) {
4869
5109
  };
4870
5110
  }
4871
5111
  function createAnswerRoute(options) {
4872
- const answer = createAnswerer(options);
5112
+ const answer2 = createAnswerer(options);
4873
5113
  return async function answerPermission(req) {
4874
5114
  const refused = answerRefusalFor(req);
4875
5115
  if (refused) {
@@ -4888,7 +5128,7 @@ function createAnswerRoute(options) {
4888
5128
  'The body must be {"sessionId": "...", "requestId": "...", "decision": "allow" or "deny"} and nothing else.'
4889
5129
  );
4890
5130
  }
4891
- const outcome = await answer(asked.sessionId, asked.requestId, asked.decision);
5131
+ const outcome = await answer2(asked.sessionId, asked.requestId, asked.decision);
4892
5132
  if (outcome !== "answered") return answerFailed(outcome);
4893
5133
  return {
4894
5134
  status: 200,
@@ -5187,7 +5427,7 @@ function createHeldAsks(options) {
5187
5427
  if (inFlight === run) inFlight = null;
5188
5428
  }
5189
5429
  }
5190
- async function answer(sessionId, requestId, decision) {
5430
+ async function answer2(sessionId, requestId, decision) {
5191
5431
  const entry = held.get(sessionId);
5192
5432
  if (!entry || entry.requestId !== requestId || !entry.confirmed) return "no-ask";
5193
5433
  if (decision === "allow" && !entry.shown.allow) return "not-allowable";
@@ -5219,7 +5459,7 @@ function createHeldAsks(options) {
5219
5459
  const verdict = verdictOf(entry);
5220
5460
  if (verdict === null) return;
5221
5461
  entry.byRule = true;
5222
- void answer(entry.sessionId, entry.requestId, verdict.decision).then((outcome) => {
5462
+ void answer2(entry.sessionId, entry.requestId, verdict.decision).then((outcome) => {
5223
5463
  if (outcome === "answered") rules.answered(request2, verdict);
5224
5464
  });
5225
5465
  }
@@ -5276,7 +5516,7 @@ function createHeldAsks(options) {
5276
5516
  void check();
5277
5517
  },
5278
5518
  check,
5279
- answer,
5519
+ answer: answer2,
5280
5520
  withAsks(snapshot) {
5281
5521
  const answering = status();
5282
5522
  if (held.size === 0) return { ...snapshot, answering };
@@ -5339,7 +5579,7 @@ function createHeldAsks(options) {
5339
5579
  import { chmod, lstat as lstat3, mkdir, stat as stat4, unlink } from "node:fs/promises";
5340
5580
  import http from "node:http";
5341
5581
  import net from "node:net";
5342
- import path9 from "node:path";
5582
+ import path10 from "node:path";
5343
5583
  var HOOK_PATH = "/hooks/permission-request";
5344
5584
  var HOOK_HEADER = "x-agent-lookout-hook";
5345
5585
  var HOOK_HEADER_VALUE = "permission-request";
@@ -5412,7 +5652,7 @@ function createHookSocket(options) {
5412
5652
  const maxBody = options.maxBodyBytes ?? MAX_HOOK_BODY_BYTES;
5413
5653
  const bodyTimeoutMs = options.bodyTimeoutMs ?? HOOK_BODY_TIMEOUT_MS;
5414
5654
  const uid = options.uid ?? process.getuid?.() ?? -1;
5415
- const folder = path9.dirname(socketPath);
5655
+ const folder = path10.dirname(socketPath);
5416
5656
  let server = null;
5417
5657
  let made = null;
5418
5658
  async function handle(req, res) {
@@ -5535,12 +5775,12 @@ function createHookSocket(options) {
5535
5775
 
5536
5776
  // src/collector/answers/registryStatus.ts
5537
5777
  import os4 from "node:os";
5538
- import path10 from "node:path";
5778
+ import path11 from "node:path";
5539
5779
  function createRegistryStatus(options) {
5540
5780
  const homeDir = options.homeDir ?? os4.homedir();
5541
5781
  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");
5782
+ const home = options.env[CLAUDE_HOME_ENV]?.trim() || path11.join(homeDir, ".claude");
5783
+ const sessionsDir = path11.join(path11.resolve(home), "sessions");
5544
5784
  return {
5545
5785
  async statusOf(sessionId) {
5546
5786
  const unknown = { status: "unknown" };
@@ -5551,7 +5791,7 @@ function createRegistryStatus(options) {
5551
5791
  const ownId = sessionId.slice("claude-code:".length);
5552
5792
  let content;
5553
5793
  try {
5554
- content = await readFile2(path10.join(sessionsDir, `${pid}.json`));
5794
+ content = await readFile2(path11.join(sessionsDir, `${pid}.json`));
5555
5795
  } catch {
5556
5796
  return unknown;
5557
5797
  }
@@ -5825,18 +6065,18 @@ function namesOwnFiles(words, ownPaths = [], folder) {
5825
6065
  (part) => from !== void 0 && !part.startsWith("/") && !part.startsWith("-") ? [part, `${from}/${part}`] : [part]
5826
6066
  );
5827
6067
  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}`));
6068
+ const path28 = plainPath(part);
6069
+ if (path28 === BY_NUMBER || path28.startsWith(`${BY_NUMBER}/`)) return true;
6070
+ if (own.some((name) => path28 === name || path28.startsWith(`${name}/`))) return true;
6071
+ if (above.has(path28)) return true;
6072
+ return ownNames.some((name) => path28 === name || path28.endsWith(`/${name}`));
5833
6073
  });
5834
6074
  });
5835
6075
  }
5836
6076
  function folderReachesOwnFiles(folder, ownPaths = []) {
5837
6077
  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}/`));
6078
+ const path28 = plainPath(folder);
6079
+ return own.some((name) => name === path28 || name.startsWith(`${path28 === "/" ? "" : path28}/`));
5840
6080
  }
5841
6081
  var PLAIN_WORD = /^[A-Za-z0-9_\-.,/:=+@%]+$/;
5842
6082
  var ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
@@ -6218,7 +6458,7 @@ function createAnswering(options) {
6218
6458
  let problem = null;
6219
6459
  let listening = false;
6220
6460
  const answerRoute = createAnswerRoute({ poller, events, asks, now: options.now });
6221
- const answer = createAnswerer({ poller, events, asks, now: options.now });
6461
+ const answer2 = createAnswerer({ poller, events, asks, now: options.now });
6222
6462
  return {
6223
6463
  async start() {
6224
6464
  const started = await socket.start();
@@ -6252,7 +6492,7 @@ function createAnswering(options) {
6252
6492
  return answerRefusalFor(req) ?? refusal(405, "This address only answers GET requests.", { Allow: "GET" });
6253
6493
  },
6254
6494
  // 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",
6495
+ answer: async (sessionId, requestId, decision) => problem === null ? answer2(sessionId, requestId, decision) : "unavailable",
6256
6496
  check: () => asks.check(),
6257
6497
  ruleAnswers: () => ruleAnswers.recent()
6258
6498
  };
@@ -6483,7 +6723,7 @@ function waitTotals(input) {
6483
6723
 
6484
6724
  // src/collector/adapters/antigravity/index.ts
6485
6725
  import os5 from "node:os";
6486
- import path14 from "node:path";
6726
+ import path15 from "node:path";
6487
6727
 
6488
6728
  // src/core/mapping/antigravityLiveness.ts
6489
6729
  function holdConversations(processes, conversations, listedAt = null) {
@@ -6522,14 +6762,15 @@ var SOURCE2 = {
6522
6762
  backend: "server.go",
6523
6763
  created: "server.go",
6524
6764
  streaming: "conversation_manager.go",
6525
- confirmation: "tool_confirmation_manager.go"
6765
+ asking: "tool_confirmation_manager.go",
6766
+ answered: "input_loop.go"
6526
6767
  };
6527
6768
  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
6769
  var OPENED = new RegExp(`^(Created|Streaming) conversation (${UUID})$`);
6529
6770
  var BACKEND = /^Creating CLI server backend: (?:.* )?workspaceDirs=\[([^\]\s]+)\](?: |$)/;
6530
6771
  var ASKING = /^Surfacing tool confirmation: "([A-Za-z]{1,64})" at step (\d{1,9})$/;
6531
6772
  var ANSWERED = new RegExp(
6532
- `^Responding to tool confirmation: convID=(${UUID}), stepIdx=(\\d{1,9}), approved=(?:true|false)$`
6773
+ `^Responding to tool confirmation: convID=(${UUID}), stepIdx=(\\d{1,9}), approved=(?:true|false)(?:, .*)?$`
6533
6774
  );
6534
6775
  var ABSOLUTE = /^(?:\/|[A-Za-z]:[\\/])/;
6535
6776
  var UNPRINTABLE = /[\p{Cc}\p{Cf}]/u;
@@ -6547,7 +6788,11 @@ function readAgyLogLine(line) {
6547
6788
  const backend = BACKEND.exec(message);
6548
6789
  return backend !== null && ABSOLUTE.test(backend[1]) && !UNPRINTABLE.test(backend[1]) ? { kind: "folder", folder: backend[1] } : null;
6549
6790
  }
6550
- if (source !== SOURCE2.confirmation) return null;
6791
+ if (source === SOURCE2.answered) {
6792
+ const answered = ANSWERED.exec(message);
6793
+ return answered === null ? null : { kind: "answered", conversation: answered[1].toLowerCase(), step: Number(answered[2]) };
6794
+ }
6795
+ if (source !== SOURCE2.asking) return null;
6551
6796
  const asking = ASKING.exec(message);
6552
6797
  if (asking !== null) {
6553
6798
  const fraction = (glog[6] ?? "0").padEnd(3, "0").slice(0, 3);
@@ -6565,10 +6810,6 @@ function readAgyLogLine(line) {
6565
6810
  }
6566
6811
  };
6567
6812
  }
6568
- const answered = ANSWERED.exec(message);
6569
- if (answered !== null) {
6570
- return { kind: "answered", conversation: answered[1].toLowerCase(), step: Number(answered[2]) };
6571
- }
6572
6813
  return null;
6573
6814
  }
6574
6815
  function emptyAgyLogState() {
@@ -6621,7 +6862,7 @@ function agyLogTimeAt(time, near) {
6621
6862
  }
6622
6863
 
6623
6864
  // src/collector/adapters/antigravity/agyLogs.ts
6624
- import path11 from "node:path";
6865
+ import path12 from "node:path";
6625
6866
  var LOG_DIR = "log";
6626
6867
  var LOG_LIMIT_BYTES = 4 * 1024 * 1024;
6627
6868
  var LINE_LIMIT_BYTES = 64 * 1024;
@@ -6656,7 +6897,7 @@ function matchLogs(processes, logs) {
6656
6897
  function createAgyLogReader(options) {
6657
6898
  const { io } = options;
6658
6899
  const now = options.now ?? Date.now;
6659
- const logDir = path11.join(options.home, LOG_DIR);
6900
+ const logDir = path12.join(options.home, LOG_DIR);
6660
6901
  const cursors = /* @__PURE__ */ new Map();
6661
6902
  let listed = /* @__PURE__ */ new Map();
6662
6903
  let listedFor = "";
@@ -6753,7 +6994,7 @@ function createAgyLogReader(options) {
6753
6994
  states.push(null);
6754
6995
  continue;
6755
6996
  }
6756
- const file = path11.join(logDir, name);
6997
+ const file = path12.join(logDir, name);
6757
6998
  kept.add(file);
6758
6999
  try {
6759
7000
  const info = await io.lstat(file);
@@ -6871,12 +7112,12 @@ function createAgyProcessReader(options = {}) {
6871
7112
  const unseen = rows.filter((row) => !known.has(keyOf(row)));
6872
7113
  const ended = /* @__PURE__ */ new Set();
6873
7114
  if (unseen.length > 0) {
6874
- const answer = await run(agyCommandLineArgs(unseen.map((row) => row.pid)));
6875
- const lines = parseCommandLines(answer.stdout);
7115
+ const answer2 = await run(agyCommandLineArgs(unseen.map((row) => row.pid)));
7116
+ const lines = parseCommandLines(answer2.stdout);
6876
7117
  for (const row of unseen) {
6877
7118
  const line = lines.get(row.pid);
6878
7119
  if (line !== void 0) known.set(keyOf(row), readAgyCommandLine(line));
6879
- else if (answer.ok || lines.size > 0) ended.add(row.pid);
7120
+ else if (answer2.ok || lines.size > 0) ended.add(row.pid);
6880
7121
  }
6881
7122
  }
6882
7123
  const running = /* @__PURE__ */ new Map();
@@ -6916,7 +7157,7 @@ function createAgyProcessReader(options = {}) {
6916
7157
  }
6917
7158
 
6918
7159
  // src/collector/adapters/antigravity/conversations.ts
6919
- import path12 from "node:path";
7160
+ import path13 from "node:path";
6920
7161
  var BRAIN_DIR = "brain";
6921
7162
  var CONVERSATIONS_DIR = "conversations";
6922
7163
  var TRANSCRIPT_IN_FOLDER = [".system_generated", "logs", "transcript.jsonl"];
@@ -6932,15 +7173,15 @@ function conversationIdOfDatabase(name) {
6932
7173
  }
6933
7174
  function createConversationFinder(options) {
6934
7175
  const { io } = options;
6935
- const brainDir = path12.join(options.home, BRAIN_DIR);
6936
- const databaseDir = path12.join(options.home, CONVERSATIONS_DIR);
7176
+ const brainDir = path13.join(options.home, BRAIN_DIR);
7177
+ const databaseDir = path13.join(options.home, CONVERSATIONS_DIR);
6937
7178
  const oldRefreshMs = options.oldRefreshMs ?? OLD_REFRESH_MS;
6938
7179
  const known = /* @__PURE__ */ new Map();
6939
7180
  let oldCheckedAt = null;
6940
7181
  async function lookAt(id) {
6941
- const transcript = path12.join(brainDir, id, ...TRANSCRIPT_IN_FOLDER);
7182
+ const transcript = path13.join(brainDir, id, ...TRANSCRIPT_IN_FOLDER);
6942
7183
  const infos = await Promise.all(
6943
- [transcript, path12.join(databaseDir, `${id}.db`), path12.join(databaseDir, `${id}.db-wal`)].map(
7184
+ [transcript, path13.join(databaseDir, `${id}.db`), path13.join(databaseDir, `${id}.db-wal`)].map(
6944
7185
  (file) => io.lstat(file).catch(() => null)
6945
7186
  )
6946
7187
  );
@@ -6996,7 +7237,7 @@ function writtenRecently(files, now) {
6996
7237
  }
6997
7238
 
6998
7239
  // src/collector/adapters/antigravity/annotations.ts
6999
- import path13 from "node:path";
7240
+ import path14 from "node:path";
7000
7241
  var ANNOTATIONS_DIR = "annotations";
7001
7242
  var ANNOTATION_LIMIT_BYTES = 4 * 1024;
7002
7243
  var TITLE_FIELD = /^title[ \t]*:[ \t]*"((?:[^"\\\n]|\\.)*)"[ \t]*$/m;
@@ -7068,10 +7309,10 @@ function readTitle(content) {
7068
7309
  }
7069
7310
  function createTitleReader(options) {
7070
7311
  const { io } = options;
7071
- const dir = path13.join(options.home, ANNOTATIONS_DIR);
7312
+ const dir = path14.join(options.home, ANNOTATIONS_DIR);
7072
7313
  const cache = /* @__PURE__ */ new Map();
7073
7314
  async function read(id) {
7074
- const file = path13.join(dir, `${id}.pbtxt`);
7315
+ const file = path14.join(dir, `${id}.pbtxt`);
7075
7316
  const info = await io.lstat(file);
7076
7317
  const known = cache.get(id);
7077
7318
  if (known && known.ino === info.ino && known.size === info.size && known.mtimeMs === info.mtimeMs) {
@@ -7654,8 +7895,8 @@ function createAntigravityAdapter(options = {}) {
7654
7895
  const processes = options.processes ?? createAgyProcessReader({ platform });
7655
7896
  const override = env[ANTIGRAVITY_HOME_ENV]?.trim() || void 0;
7656
7897
  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);
7898
+ const home = path15.resolve(override ?? path15.join(homeDir, ...DEFAULT_HOME));
7899
+ const brainDir = path15.join(home, BRAIN_DIR);
7659
7900
  const homeName = tildify(home, homeDir);
7660
7901
  const brainName = tildify(brainDir, homeDir);
7661
7902
  const finder = createConversationFinder({ home, io, oldRefreshMs: options.oldRefreshMs });
@@ -7820,8 +8061,8 @@ function createAntigravityAdapter(options = {}) {
7820
8061
  basis = BASIS_UNMATCHED;
7821
8062
  }
7822
8063
  if (unknownStep) detail += ` ${UNKNOWN_STEP_NOTE}`;
7823
- const answer = result("ok", detail, every(pollIntervalMs), checkedAt, basis);
7824
- return { ...answer, sessions };
8064
+ const answer2 = result("ok", detail, every(pollIntervalMs), checkedAt, basis);
8065
+ return { ...answer2, sessions };
7825
8066
  }
7826
8067
  return {
7827
8068
  id: SOURCE_ID2,
@@ -7845,7 +8086,7 @@ function createAntigravityAdapter(options = {}) {
7845
8086
 
7846
8087
  // src/collector/adapters/codex/index.ts
7847
8088
  import os6 from "node:os";
7848
- import path16 from "node:path";
8089
+ import path17 from "node:path";
7849
8090
 
7850
8091
  // src/core/mapping/codexMapping.ts
7851
8092
  var TURN_UNDER_WAY = /* @__PURE__ */ new Set(["task_started", "turn_started"]);
@@ -8162,7 +8403,7 @@ function createRolloutReader(io) {
8162
8403
  }
8163
8404
 
8164
8405
  // src/collector/adapters/codex/rollouts.ts
8165
- import path15 from "node:path";
8406
+ import path16 from "node:path";
8166
8407
  var UUID4 = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
8167
8408
  var ROLLOUT_FILE_NAME = new RegExp(
8168
8409
  `^rollout-\\d{4}-\\d{2}-\\d{2}T\\d{2}-\\d{2}-\\d{2}-(${UUID4})(?:_(${UUID4}))?\\.jsonl$`,
@@ -8180,7 +8421,7 @@ function twoDigits(value) {
8180
8421
  function recentDayFolders(sessionsDir, now) {
8181
8422
  const folders = [now, now - 24 * 60 * 60 * 1e3].map((at) => {
8182
8423
  const date = new Date(at);
8183
- return path15.join(
8424
+ return path16.join(
8184
8425
  sessionsDir,
8185
8426
  String(date.getFullYear()),
8186
8427
  twoDigits(date.getMonth() + 1),
@@ -8209,12 +8450,12 @@ function createRolloutFinder(options) {
8209
8450
  const refs = [];
8210
8451
  for (const name of (await names(dir)).sort()) {
8211
8452
  const threadId = threadIdOfRollout(name);
8212
- if (threadId !== null) refs.push({ path: path15.join(dir, name), threadId });
8453
+ if (threadId !== null) refs.push({ path: path16.join(dir, name), threadId });
8213
8454
  }
8214
8455
  return refs;
8215
8456
  }
8216
8457
  async function subfolders(dir, pattern) {
8217
- return (await names(dir)).filter((name) => pattern.test(name)).sort().map((name) => path15.join(dir, name));
8458
+ return (await names(dir)).filter((name) => pattern.test(name)).sort().map((name) => path16.join(dir, name));
8218
8459
  }
8219
8460
  async function buildIndex() {
8220
8461
  const years = await subfolders(sessionsDir, YEAR);
@@ -8433,10 +8674,10 @@ function createCodexAdapter(options = {}) {
8433
8674
  const ownOverride = env[CODEX_HOME_ENV]?.trim() || void 0;
8434
8675
  const codexOverride = env[CODEX_OWN_HOME_ENV]?.trim() || void 0;
8435
8676
  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);
8677
+ const codexHome = path17.resolve(ownOverride ?? codexOverride ?? path17.join(homeDir, ".codex"));
8678
+ const sessionsDir = path17.join(codexHome, "sessions");
8679
+ const locksDir = path17.join(codexHome, WRITER_LOCK_DIR);
8680
+ const indexFile = path17.join(codexHome, SESSION_INDEX_FILE);
8440
8681
  const homeName = tildify(codexHome, homeDir);
8441
8682
  const sessionsName = tildify(sessionsDir, homeDir);
8442
8683
  const locksName = tildify(locksDir, homeDir);
@@ -8619,8 +8860,8 @@ function createCodexAdapter(options = {}) {
8619
8860
  } else if (olderCodex) {
8620
8861
  detail += ` ${OLDER_CODEX_NOTE}`;
8621
8862
  }
8622
- const answer = result("ok", detail, every(pollIntervalMs), checkedAt);
8623
- return { ...answer, sessions, basis: locks.supported ? BASIS2 : BASIS_WITHOUT_LOCKS };
8863
+ const answer2 = result("ok", detail, every(pollIntervalMs), checkedAt);
8864
+ return { ...answer2, sessions, basis: locks.supported ? BASIS2 : BASIS_WITHOUT_LOCKS };
8624
8865
  }
8625
8866
  return {
8626
8867
  id: SOURCE_ID3,
@@ -8644,7 +8885,7 @@ function createCodexAdapter(options = {}) {
8644
8885
 
8645
8886
  // src/collector/adapters/status-files/index.ts
8646
8887
  import os7 from "node:os";
8647
- import path17 from "node:path";
8888
+ import path18 from "node:path";
8648
8889
 
8649
8890
  // src/core/mapping/statusFileMapping.ts
8650
8891
  var STATUS = /* @__PURE__ */ new Map([
@@ -8793,7 +9034,7 @@ function createStatusFileAdapter(options = {}) {
8793
9034
  const isAlive = options.isAlive ?? isProcessAlive;
8794
9035
  const pollIntervalMs = options.pollIntervalMs ?? POLL_INTERVAL_MS;
8795
9036
  const override = env[STATUS_DIR_ENV]?.trim() || void 0;
8796
- const dir = path17.resolve(override ?? path17.join(homeDir, ".agent-lookout", "sessions"));
9037
+ const dir = path18.resolve(override ?? path18.join(homeDir, ".agent-lookout", "sessions"));
8797
9038
  const dirName = tildify(dir, homeDir);
8798
9039
  const decoder2 = new TextDecoder();
8799
9040
  let lastRead = /* @__PURE__ */ new Map();
@@ -8853,7 +9094,7 @@ function createStatusFileAdapter(options = {}) {
8853
9094
  if (names.length <= MAX_FILES) return names;
8854
9095
  const written = await Promise.all(
8855
9096
  names.map(async (name) => {
8856
- const info = await io.lstat(path17.join(dir, name)).catch(() => null);
9097
+ const info = await io.lstat(path18.join(dir, name)).catch(() => null);
8857
9098
  return { name, mtimeMs: info?.mtimeMs ?? -Infinity };
8858
9099
  })
8859
9100
  );
@@ -8874,14 +9115,14 @@ function createStatusFileAdapter(options = {}) {
8874
9115
  names = await io.readdir(dir);
8875
9116
  } catch (error) {
8876
9117
  if (!isMissing2(error)) {
8877
- const answer = result(
9118
+ const answer2 = result(
8878
9119
  "error",
8879
9120
  `Status files could not be read: the folder ${dirName} could not be listed.`,
8880
9121
  watching("cannot be read"),
8881
9122
  checkedAt
8882
9123
  );
8883
- answer.health.advice = "Check that your user account can open the folder.";
8884
- return answer;
9124
+ answer2.health.advice = "Check that your user account can open the folder.";
9125
+ return answer2;
8885
9126
  }
8886
9127
  contentsKnown = true;
8887
9128
  lastRead = /* @__PURE__ */ new Map();
@@ -8903,7 +9144,7 @@ function createStatusFileAdapter(options = {}) {
8903
9144
  const nextRead = /* @__PURE__ */ new Map();
8904
9145
  const nextStatuses = /* @__PURE__ */ new Map();
8905
9146
  for (const fileName2 of await toRead(listed)) {
8906
- const reading = await readOne(path17.join(dir, fileName2));
9147
+ const reading = await readOne(path18.join(dir, fileName2));
8907
9148
  let kept;
8908
9149
  if (reading.kind === "read") {
8909
9150
  kept = reading;
@@ -9130,7 +9371,7 @@ function createEventStore(capacity = EVENT_CAPACITY) {
9130
9371
 
9131
9372
  // src/collector/git/branchFinder.ts
9132
9373
  import os8 from "node:os";
9133
- import path19 from "node:path";
9374
+ import path20 from "node:path";
9134
9375
 
9135
9376
  // src/collector/git/gitHead.ts
9136
9377
  var MAX_GIT_FILE_BYTES = 4 * 1024;
@@ -9170,18 +9411,18 @@ function parseCommonDir(content) {
9170
9411
 
9171
9412
  // src/collector/git/repository.ts
9172
9413
  import { createHash as createHash3 } from "node:crypto";
9173
- import path18 from "node:path";
9414
+ import path19 from "node:path";
9174
9415
  var REPOSITORY_ID_LENGTH = 16;
9175
9416
  function repositoryId(gitFolder) {
9176
9417
  return createHash3("sha256").update(gitFolder).digest("hex").slice(0, REPOSITORY_ID_LENGTH);
9177
9418
  }
9178
9419
  function sharedName(common) {
9179
- const base = path18.basename(common);
9180
- if (base.startsWith(".")) return path18.basename(path18.dirname(common));
9420
+ const base = path19.basename(common);
9421
+ if (base.startsWith(".")) return path19.basename(path19.dirname(common));
9181
9422
  return base.endsWith(".git") ? base.slice(0, -".git".length) : base;
9182
9423
  }
9183
9424
  function repositoryOf({ dir, gitDir, commonDir }) {
9184
- const name = sessionName(commonDir === null ? path18.basename(dir) : sharedName(commonDir));
9425
+ const name = sessionName(commonDir === null ? path19.basename(dir) : sharedName(commonDir));
9185
9426
  return name === void 0 ? null : { id: repositoryId(commonDir ?? gitDir), name };
9186
9427
  }
9187
9428
 
@@ -9210,13 +9451,13 @@ async function readGitFile(io, file, maxBytes = MAX_GIT_FILE_BYTES) {
9210
9451
  }
9211
9452
  }
9212
9453
  async function commonDirOf(io, gitDir) {
9213
- const content = await readGitFile(io, path19.join(gitDir, "commondir"));
9454
+ const content = await readGitFile(io, path20.join(gitDir, "commondir"));
9214
9455
  if (content === void 0) return null;
9215
9456
  const target = content === null ? null : parseCommonDir(content);
9216
- return target === null ? void 0 : path19.resolve(gitDir, target);
9457
+ return target === null ? void 0 : path20.resolve(gitDir, target);
9217
9458
  }
9218
9459
  async function headIn(dir, kind, io) {
9219
- const marker = path19.join(dir, ".git");
9460
+ const marker = path20.join(dir, ".git");
9220
9461
  let gitDir;
9221
9462
  if (kind === "directory") {
9222
9463
  gitDir = marker;
@@ -9224,11 +9465,11 @@ async function headIn(dir, kind, io) {
9224
9465
  const content2 = await readGitFile(io, marker);
9225
9466
  const target = typeof content2 === "string" ? parseGitFile(content2) : null;
9226
9467
  if (target === null) return null;
9227
- gitDir = path19.resolve(dir, target);
9468
+ gitDir = path20.resolve(dir, target);
9228
9469
  } else {
9229
9470
  return null;
9230
9471
  }
9231
- const content = await readGitFile(io, path19.join(gitDir, "HEAD"));
9472
+ const content = await readGitFile(io, path20.join(gitDir, "HEAD"));
9232
9473
  const head = typeof content === "string" ? parseHead(content) : null;
9233
9474
  if (head === null) return null;
9234
9475
  const commonDir = kind === "file" ? await commonDirOf(io, gitDir) : null;
@@ -9237,20 +9478,20 @@ async function headIn(dir, kind, io) {
9237
9478
  return { head: { ...head, repository }, gitFolder: commonDir ?? gitDir };
9238
9479
  }
9239
9480
  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);
9481
+ if (!path20.isAbsolute(cwd)) return null;
9482
+ const home = path20.resolve(homeDir);
9483
+ let dir = path20.resolve(cwd);
9243
9484
  try {
9244
9485
  if ((await io.stat(dir)).kind !== "directory") return null;
9245
9486
  } catch {
9246
9487
  return null;
9247
9488
  }
9248
9489
  for (let level = 0; level < MAX_LEVELS; level += 1) {
9249
- const parent = path19.dirname(dir);
9490
+ const parent = path20.dirname(dir);
9250
9491
  if (dir === home || dir === parent) return null;
9251
9492
  let kind;
9252
9493
  try {
9253
- kind = (await io.lstat(path19.join(dir, ".git"))).kind;
9494
+ kind = (await io.lstat(path20.join(dir, ".git"))).kind;
9254
9495
  } catch (error) {
9255
9496
  if (!isMissing2(error)) return null;
9256
9497
  dir = parent;
@@ -9435,11 +9676,10 @@ function ghArguments(target) {
9435
9676
  ];
9436
9677
  }
9437
9678
  var LEFT_OUT = ["GH_FORCE_TTY", "GH_DEBUG", "DEBUG", "GH_PAGER", "PAGER"];
9438
- var OWN_SETTING = /^AGENT_LOOKOUT_/;
9439
9679
  function ghEnvironment(env) {
9440
9680
  const given = {};
9441
- for (const [name, value] of Object.entries(env)) {
9442
- if (!OWN_SETTING.test(name) && !LEFT_OUT.includes(name)) given[name] = value;
9681
+ for (const [name, value] of Object.entries(childEnvironment(env))) {
9682
+ if (!LEFT_OUT.includes(name)) given[name] = value;
9443
9683
  }
9444
9684
  return {
9445
9685
  ...given,
@@ -9528,7 +9768,7 @@ function createGhAsker(options) {
9528
9768
  }
9529
9769
 
9530
9770
  // src/collector/git/pullRequestTarget.ts
9531
- import path20 from "node:path";
9771
+ import path21 from "node:path";
9532
9772
 
9533
9773
  // src/collector/git/gitConfig.ts
9534
9774
  var MAX_CONFIG_BYTES = 64 * 1024;
@@ -9786,7 +10026,7 @@ function targetOf(config, branch, base, defaultBranch) {
9786
10026
  async function readPullRequestTarget(gitFolder, branch, io = nodeIo2) {
9787
10027
  try {
9788
10028
  if (!isAskableBranch(branch)) return null;
9789
- const text4 = await readGitFile(io, path20.join(gitFolder, "config"), MAX_CONFIG_BYTES);
10029
+ const text4 = await readGitFile(io, path21.join(gitFolder, "config"), MAX_CONFIG_BYTES);
9790
10030
  if (typeof text4 !== "string") return null;
9791
10031
  const config = parseGitConfig(text4);
9792
10032
  const base = config === null ? null : baseRemoteOf(config);
@@ -9795,7 +10035,7 @@ async function readPullRequestTarget(gitFolder, branch, io = nodeIo2) {
9795
10035
  if (base.name !== null && isSafeRemoteName(base.name)) {
9796
10036
  const head = await readGitFile(
9797
10037
  io,
9798
- path20.join(gitFolder, "refs", "remotes", base.name, "HEAD")
10038
+ path21.join(gitFolder, "refs", "remotes", base.name, "HEAD")
9799
10039
  );
9800
10040
  if (typeof head === "string") defaultBranch = parseRemoteHead(head, base.name);
9801
10041
  }
@@ -9830,19 +10070,19 @@ function createPullRequestFinder(options) {
9830
10070
  let gh = "unknown";
9831
10071
  let last = null;
9832
10072
  const due = (at, since) => at - (since ?? -Infinity) >= intervalMs;
9833
- function take(question, answer, at) {
9834
- switch (answer.kind) {
10073
+ function take(question, answer2, at) {
10074
+ switch (answer2.kind) {
9835
10075
  case "found":
9836
10076
  case "none":
9837
10077
  gh = "ready";
9838
10078
  last = { at, ok: true };
9839
10079
  answers2.set(question, {
9840
- pullRequest: answer.kind === "found" ? answer.pullRequest : null,
10080
+ pullRequest: answer2.kind === "found" ? answer2.pullRequest : null,
9841
10081
  askedAt: at
9842
10082
  });
9843
10083
  return true;
9844
10084
  case "failed":
9845
- last = { at, ok: false, reason: answer.reason };
10085
+ last = { at, ok: false, reason: answer2.reason };
9846
10086
  answers2.set(question, {
9847
10087
  pullRequest: answers2.get(question)?.pullRequest ?? null,
9848
10088
  askedAt: at
@@ -9850,7 +10090,7 @@ function createPullRequestFinder(options) {
9850
10090
  return true;
9851
10091
  case "not-found":
9852
10092
  case "signed-out":
9853
- gh = answer.kind;
10093
+ gh = answer2.kind;
9854
10094
  answers2.clear();
9855
10095
  pausedUntil = at + intervalMs;
9856
10096
  return false;
@@ -9870,14 +10110,14 @@ function createPullRequestFinder(options) {
9870
10110
  const at = now();
9871
10111
  if (at < pausedUntil) return;
9872
10112
  if (!due(at, answers2.get(question)?.askedAt)) continue;
9873
- const answer = await ask(target).catch(() => ({
10113
+ const answer2 = await ask(target).catch(() => ({
9874
10114
  kind: "failed",
9875
10115
  reason: "gh could not be run"
9876
10116
  }));
9877
10117
  if (![...reads.values()].some((read) => read.target && questionOf(read.target) === question)) {
9878
10118
  continue;
9879
10119
  }
9880
- if (!take(question, answer, now())) return;
10120
+ if (!take(question, answer2, now())) return;
9881
10121
  }
9882
10122
  }
9883
10123
  return {
@@ -9966,7 +10206,7 @@ function createClearHistoryRoute(options) {
9966
10206
  }
9967
10207
 
9968
10208
  // src/collector/history/historyKeeper.ts
9969
- import path21 from "node:path";
10209
+ import path22 from "node:path";
9970
10210
 
9971
10211
  // src/collector/history/historyFiles.ts
9972
10212
  import {
@@ -10266,7 +10506,7 @@ function createHistoryKeeper(options) {
10266
10506
  });
10267
10507
  const lock = createWriterLock({
10268
10508
  fs,
10269
- file: path21.join(dir, LOCK_FILE),
10509
+ file: path22.join(dir, LOCK_FILE),
10270
10510
  pid: options.pid,
10271
10511
  now,
10272
10512
  isAlive: options.isAlive,
@@ -10292,7 +10532,7 @@ function createHistoryKeeper(options) {
10292
10532
  });
10293
10533
  return next;
10294
10534
  }
10295
- const full = (name) => path21.join(dir, name);
10535
+ const full = (name) => path22.join(dir, name);
10296
10536
  function hold(record) {
10297
10537
  if (unusable !== null || !running) return;
10298
10538
  const line = encodeRecord(record);
@@ -10754,13 +10994,13 @@ function keptHistoryStore(store, keeper) {
10754
10994
  // src/collector/history/historySettings.ts
10755
10995
  import { createHash as createHash4 } from "node:crypto";
10756
10996
  import os9 from "node:os";
10757
- import path22 from "node:path";
10997
+ import path23 from "node:path";
10758
10998
  var HISTORY_ENV = "AGENT_LOOKOUT_HISTORY";
10759
10999
  var HISTORY_DIR_ENV = "AGENT_LOOKOUT_HISTORY_DIR";
10760
11000
  function readHistorySetup(env, homeDir = os9.homedir()) {
10761
11001
  if (env[HISTORY_ENV]?.trim().toLowerCase() === "off") return { on: false };
10762
11002
  const override = env[HISTORY_DIR_ENV]?.trim() || void 0;
10763
- const dir = path22.resolve(override ?? path22.join(homeDir, ".agent-lookout", "history"));
11003
+ const dir = path23.resolve(override ?? path23.join(homeDir, ".agent-lookout", "history"));
10764
11004
  return {
10765
11005
  on: true,
10766
11006
  dir,
@@ -10771,14 +11011,57 @@ function readHistorySetup(env, homeDir = os9.homedir()) {
10771
11011
  function sourcesFingerprint(env, homeDir = os9.homedir()) {
10772
11012
  const set = (name) => env[name]?.trim() || void 0;
10773
11013
  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))
11014
+ path23.resolve(set(CLAUDE_HOME_ENV) ?? path23.join(homeDir, ".claude")),
11015
+ path23.resolve(set(CODEX_HOME_ENV) ?? set(CODEX_OWN_HOME_ENV) ?? path23.join(homeDir, ".codex")),
11016
+ path23.resolve(set(STATUS_DIR_ENV) ?? path23.join(homeDir, ".agent-lookout", "sessions")),
11017
+ path23.resolve(set(ANTIGRAVITY_HOME_ENV) ?? path23.join(homeDir, ...DEFAULT_HOME))
10778
11018
  ];
10779
11019
  return createHash4("sha256").update(JSON.stringify(folders)).digest("hex").slice(0, 32);
10780
11020
  }
10781
11021
 
11022
+ // src/collector/messages/lastMessageRoute.ts
11023
+ var answer = (body) => ({ status: 200, body });
11024
+ var failed3 = (status, error, headers) => ({
11025
+ status,
11026
+ body: { error },
11027
+ ...headers && { headers }
11028
+ });
11029
+ function idIn(query) {
11030
+ const pairs = [...query];
11031
+ if (pairs.length !== 1) return null;
11032
+ const [name, value] = pairs[0] ?? ["", ""];
11033
+ if (name !== "id" || value === "" || value.length > MAX_SESSION_ID_LENGTH) return null;
11034
+ return value;
11035
+ }
11036
+ function createLastMessageRoute(options) {
11037
+ const { poller, adapters } = options;
11038
+ const off = lastMessageOff(options.env);
11039
+ return async (req, url) => {
11040
+ const site = req.headers["sec-fetch-site"];
11041
+ if (site !== void 0 && site !== "same-origin") {
11042
+ return failed3(403, "This address only answers the dashboard page served from this machine.");
11043
+ }
11044
+ const id = idIn(url.searchParams);
11045
+ if (id === null) {
11046
+ return failed3(400, "Name one session as id, and nothing else.");
11047
+ }
11048
+ if (off) return answer({ message: null, reason: "off", setting: LAST_MESSAGE_ENV });
11049
+ const session = poller.getSnapshot().sessions.find((listed) => listed.id === id);
11050
+ const notListed = () => failed3(404, "No session with that id is listed.");
11051
+ if (session === void 0) return notListed();
11052
+ const adapter = adapters.find((candidate) => candidate.id === session.source);
11053
+ if (adapter?.lastMessage === void 0) return answer({ message: null, reason: "not-read" });
11054
+ const read = await adapter.lastMessage(session.id);
11055
+ if (read === null) return notListed();
11056
+ if (read === "busy") {
11057
+ return failed3(429, "Too many last messages were asked for at once. Ask again in a second.", {
11058
+ "Retry-After": "1"
11059
+ });
11060
+ }
11061
+ return answer(read);
11062
+ };
11063
+ }
11064
+
10782
11065
  // src/collector/notifications/heldWait.ts
10783
11066
  var HANDOVER_GRACE_MS = 3e3;
10784
11067
  var PAGE_GONE_AFTER_MS = 5e3;
@@ -10944,6 +11227,7 @@ function runOsascript(args, options = {}) {
10944
11227
  OSASCRIPT,
10945
11228
  [...args],
10946
11229
  {
11230
+ env: childEnvironment(process.env),
10947
11231
  timeout: options.timeoutMs ?? TERMINAL_JUMP_TIMEOUT_MS,
10948
11232
  maxBuffer: MAX_OUTPUT_BYTES4,
10949
11233
  encoding: "utf8",
@@ -11266,7 +11550,7 @@ function channelIn(body) {
11266
11550
  function asSentence(reason) {
11267
11551
  return `${reason.charAt(0).toUpperCase()}${reason.slice(1)}.`;
11268
11552
  }
11269
- var failed3 = (status, reason, error, more = {}) => ({
11553
+ var failed4 = (status, reason, error, more = {}) => ({
11270
11554
  status,
11271
11555
  body: {
11272
11556
  error,
@@ -11298,21 +11582,21 @@ function createTestSendRoute(options) {
11298
11582
  const { name, setting } = NAMED[channel];
11299
11583
  const tester = channels[channel];
11300
11584
  if (tester === null) {
11301
- return failed3(
11585
+ return failed4(
11302
11586
  409,
11303
11587
  "off",
11304
11588
  `${name} is not set up. Set ${setting} and start Agent Lookout again.`
11305
11589
  );
11306
11590
  }
11307
11591
  if (!limiter.begin()) {
11308
- return failed3(429, "too-soon", "One test is sent a second. Try again in a moment.", {
11592
+ return failed4(429, "too-soon", "One test is sent a second. Try again in a moment.", {
11309
11593
  retryAfter: 1
11310
11594
  });
11311
11595
  }
11312
11596
  try {
11313
11597
  const result = await tester.test();
11314
11598
  if (!result.tried) {
11315
- return failed3(
11599
+ return failed4(
11316
11600
  429,
11317
11601
  "limited",
11318
11602
  `${SENDS_PER_HOUR} ${name} pushes were tried in the last hour, the most it tries.`,
@@ -11323,7 +11607,7 @@ function createTestSendRoute(options) {
11323
11607
  );
11324
11608
  }
11325
11609
  if (!result.outcome.sent) {
11326
- return failed3(502, "not-sent", asSentence(result.outcome.reason));
11610
+ return failed4(502, "not-sent", asSentence(result.outcome.reason));
11327
11611
  }
11328
11612
  return {
11329
11613
  status: 200,
@@ -11423,12 +11707,12 @@ function askRemote(port, route, timeoutMs = REMOTE_ANSWER_TIMEOUT_MS) {
11423
11707
  return new Promise((resolve) => {
11424
11708
  let connected = false;
11425
11709
  let settled = false;
11426
- const finish = (answer) => {
11710
+ const finish = (answer2) => {
11427
11711
  if (settled) return;
11428
11712
  settled = true;
11429
11713
  clearTimeout(timer);
11430
11714
  req.destroy();
11431
- resolve(answer);
11715
+ resolve(answer2);
11432
11716
  };
11433
11717
  const req = request({
11434
11718
  host: "127.0.0.1",
@@ -11624,7 +11908,7 @@ function readRemoteSnapshot(value, machine, now, options = {}) {
11624
11908
  }
11625
11909
 
11626
11910
  // src/collector/remotes/sshProgram.ts
11627
- import path23 from "node:path";
11911
+ import path24 from "node:path";
11628
11912
  var SSH_BIN_ENV = "AGENT_LOOKOUT_SSH_BIN";
11629
11913
  var SSH_LOCATIONS = ["/usr/bin/ssh", "/opt/homebrew/bin/ssh", "/usr/local/bin/ssh"];
11630
11914
  var SERVER_ALIVE_SECONDS = 15;
@@ -11646,7 +11930,7 @@ async function findSsh(env, isExecutable, platform = process.platform) {
11646
11930
  }
11647
11931
  return {
11648
11932
  found: false,
11649
- looked: `The ssh command was not found on PATH or in ${SSH_LOCATIONS.map((at) => path23.dirname(at)).join(", ")}`
11933
+ looked: `The ssh command was not found on PATH or in ${SSH_LOCATIONS.map((at) => path24.dirname(at)).join(", ")}`
11650
11934
  };
11651
11935
  }
11652
11936
  function sshArguments(forward) {
@@ -11988,7 +12272,11 @@ function freeLoopbackPort() {
11988
12272
  });
11989
12273
  });
11990
12274
  }
11991
- var spawnSsh = (binary, args, env) => spawn2(binary, [...args], { env, stdio: ["ignore", "ignore", "pipe"], windowsHide: true });
12275
+ var spawnSsh = (binary, args, env) => spawn2(binary, [...args], {
12276
+ env: childEnvironment(env),
12277
+ stdio: ["ignore", "ignore", "pipe"],
12278
+ windowsHide: true
12279
+ });
11992
12280
  var live = /* @__PURE__ */ new Set();
11993
12281
  var endedOnExit = false;
11994
12282
  function endOnExit(child) {
@@ -12242,14 +12530,14 @@ import {
12242
12530
  writeSync as writeSync2
12243
12531
  } from "node:fs";
12244
12532
  import os10 from "node:os";
12245
- import path24 from "node:path";
12533
+ import path25 from "node:path";
12246
12534
  var SETTINGS_FILE_ENV = "AGENT_LOOKOUT_SETTINGS_FILE";
12247
12535
  var SETTINGS_FILE_MODE = 384;
12248
12536
  var SETTINGS_FOLDER_MODE = 448;
12249
12537
  var MAX_SETTINGS_BYTES = 64 * 1024;
12250
12538
  function readSettingsSetup(env, homeDir = os10.homedir()) {
12251
12539
  const named = env[SETTINGS_FILE_ENV]?.trim() || void 0;
12252
- const file = path24.resolve(named ?? path24.join(homeDir, ".agent-lookout", "settings.json"));
12540
+ const file = path25.resolve(named ?? path25.join(homeDir, ".agent-lookout", "settings.json"));
12253
12541
  return { file, shown: tildify(file, homeDir) };
12254
12542
  }
12255
12543
  function codeOf(error) {
@@ -12294,7 +12582,7 @@ function readSettingsStamp(setup) {
12294
12582
  }
12295
12583
  function readSettingsText(setup) {
12296
12584
  const { file, shown } = setup;
12297
- const folder = path24.dirname(file);
12585
+ const folder = path25.dirname(file);
12298
12586
  try {
12299
12587
  const folderKind = kindAt(folder);
12300
12588
  if (folderKind === null) return { kind: "missing" };
@@ -12353,7 +12641,7 @@ function readSettingsText(setup) {
12353
12641
  }
12354
12642
  function writeSettingsText(setup, text4) {
12355
12643
  const { file, shown } = setup;
12356
- const folder = path24.dirname(file);
12644
+ const folder = path25.dirname(file);
12357
12645
  const notSaved = {
12358
12646
  ok: false,
12359
12647
  problem: `${shown} could not be written, so the change was not saved.`
@@ -12374,9 +12662,9 @@ function writeSettingsText(setup, text4) {
12374
12662
  problem: `${shown} is a link or not an ordinary file, which Agent Lookout does not write through, so the change was not saved.`
12375
12663
  };
12376
12664
  }
12377
- temporary = path24.join(
12665
+ temporary = path25.join(
12378
12666
  folder,
12379
- `.${path24.basename(file)}.${process.pid}.${randomBytes3(6).toString("hex")}.tmp`
12667
+ `.${path25.basename(file)}.${process.pid}.${randomBytes3(6).toString("hex")}.tmp`
12380
12668
  );
12381
12669
  const fd = openWithoutFollowingNow(
12382
12670
  temporary,
@@ -12655,7 +12943,7 @@ function applyRulesChange(rules, change, newId) {
12655
12943
 
12656
12944
  // src/collector/settings/permissionRulesRoute.ts
12657
12945
  var MAX_PERMISSION_RULES_BODY_BYTES = 1024;
12658
- var failed4 = (status, reason, error) => ({
12946
+ var failed5 = (status, reason, error) => ({
12659
12947
  status,
12660
12948
  body: { error, reason }
12661
12949
  });
@@ -12682,14 +12970,14 @@ function createPermissionRulesRoute(options) {
12682
12970
  try {
12683
12971
  value = JSON.parse(body.text);
12684
12972
  } catch {
12685
- return failed4(400, "invalid", "The body must be JSON: one change to the permission rules.");
12973
+ return failed5(400, "invalid", "The body must be JSON: one change to the permission rules.");
12686
12974
  }
12687
12975
  const asked = rulesChangeIn(value);
12688
- if (!asked.ok) return failed4(400, "invalid", asked.problem);
12976
+ if (!asked.ok) return failed5(400, "invalid", asked.problem);
12689
12977
  const made = settings.changePermissionRules(
12690
12978
  (rules) => applyRulesChange(rules, asked.change, newId)
12691
12979
  );
12692
- if (!made.ok) return failed4(STATUS_OF[made.reason], made.reason, made.problem);
12980
+ if (!made.ok) return failed5(STATUS_OF[made.reason], made.reason, made.problem);
12693
12981
  return {
12694
12982
  status: 200,
12695
12983
  body: { ok: true, permissionRules: made.rules }
@@ -12699,7 +12987,7 @@ function createPermissionRulesRoute(options) {
12699
12987
 
12700
12988
  // src/collector/settings/timeRulesRoute.ts
12701
12989
  var MAX_TIME_RULES_BODY_BYTES = 2048;
12702
- var failed5 = (status, reason, error) => ({
12990
+ var failed6 = (status, reason, error) => ({
12703
12991
  status,
12704
12992
  body: { error, reason }
12705
12993
  });
@@ -12723,12 +13011,12 @@ function createTimeRulesRoute(options) {
12723
13011
  try {
12724
13012
  value = JSON.parse(body.text);
12725
13013
  } catch {
12726
- return failed5(400, "invalid", "The body must be JSON: the three time rules.");
13014
+ return failed6(400, "invalid", "The body must be JSON: the three time rules.");
12727
13015
  }
12728
13016
  const asked = timeRulesIn(value);
12729
- if (!asked.ok) return failed5(400, "invalid", asked.problem);
13017
+ if (!asked.ok) return failed6(400, "invalid", asked.problem);
12730
13018
  const saved = settings.changeTimeRules(asked.rules);
12731
- if (!saved.ok) return failed5(500, "not-saved", saved.problem);
13019
+ if (!saved.ok) return failed6(500, "not-saved", saved.problem);
12732
13020
  return {
12733
13021
  status: 200,
12734
13022
  body: { ok: true, timeRules: asked.rules }
@@ -12751,8 +13039,8 @@ function parseProcessTable(stdout) {
12751
13039
  }
12752
13040
  var readProcessTableWithPs = async () => {
12753
13041
  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();
13042
+ const answer2 = await runPs(PS_TABLE_ARGS);
13043
+ return answer2.ok ? parseProcessTable(answer2.stdout) : /* @__PURE__ */ new Map();
12756
13044
  };
12757
13045
  function parentsIn(table) {
12758
13046
  const parents = /* @__PURE__ */ new Map();
@@ -12896,7 +13184,7 @@ function runTmuxBinary(binary, args, options) {
12896
13184
  binary,
12897
13185
  [...args],
12898
13186
  {
12899
- env: options.env,
13187
+ env: childEnvironment(options.env),
12900
13188
  timeout: options.timeoutMs ?? TMUX_TIMEOUT_MS,
12901
13189
  maxBuffer: MAX_OUTPUT_BYTES5,
12902
13190
  encoding: "utf8",
@@ -13304,6 +13592,10 @@ function createCollector(options) {
13304
13592
  }),
13305
13593
  pullRequests: () => pullRequests?.status() ?? pullRequestsOff,
13306
13594
  jump: createJumpRoute({ poller, panes, run: tmux, tabs, osascript, now }),
13595
+ // Only this computer's own adapters read what a session last said. A
13596
+ // session on another machine is answered as not read, and nothing is
13597
+ // asked of that machine.
13598
+ lastMessage: createLastMessageRoute({ env, poller, adapters: localAdapters }),
13307
13599
  stop: stopRoutes && createStopRoute(stopRoutes),
13308
13600
  cleanUp: stopRoutes && createCleanUpRoute(stopRoutes),
13309
13601
  settings: () => ({
@@ -13433,7 +13725,7 @@ async function runStandalone(options) {
13433
13725
  } catch (error) {
13434
13726
  return stopWith(error.message);
13435
13727
  }
13436
- if (!existsSync(path25.join(distDir, "index.html"))) {
13728
+ if (!existsSync(path26.join(distDir, "index.html"))) {
13437
13729
  return stopWith(
13438
13730
  "The dashboard has not been built yet. Run `npm run build`, then start it again."
13439
13731
  );
@@ -13476,12 +13768,12 @@ async function runStandalone(options) {
13476
13768
 
13477
13769
  // src/cli/start/openBrowser.ts
13478
13770
  import { spawn as spawn3 } from "node:child_process";
13479
- import path26 from "node:path";
13771
+ import path27 from "node:path";
13480
13772
  var OPEN_WAIT_MS = 1e4;
13481
13773
  var WINDOWS_FOLDER = "C:\\Windows";
13482
13774
  function windowsFolder(env) {
13483
13775
  const named = env.SystemRoot?.trim() || env.SYSTEMROOT?.trim();
13484
- return named && path26.win32.isAbsolute(named) ? named : WINDOWS_FOLDER;
13776
+ return named && path27.win32.isAbsolute(named) ? named : WINDOWS_FOLDER;
13485
13777
  }
13486
13778
  function openCommand(platform, url, env = process.env) {
13487
13779
  const address = loopbackAddress(url);
@@ -13490,7 +13782,7 @@ function openCommand(platform, url, env = process.env) {
13490
13782
  if (platform === "linux") return { file: "xdg-open", args: [address] };
13491
13783
  if (platform === "win32") {
13492
13784
  return {
13493
- file: path26.win32.join(windowsFolder(env), "System32", "rundll32.exe"),
13785
+ file: path27.win32.join(windowsFolder(env), "System32", "rundll32.exe"),
13494
13786
  args: ["url.dll,FileProtocolHandler", address]
13495
13787
  };
13496
13788
  }
@@ -13504,7 +13796,12 @@ function runProgram2(file, args, waitMs = OPEN_WAIT_MS) {
13504
13796
  resolve(opened);
13505
13797
  };
13506
13798
  try {
13507
- const child = spawn3(file, [...args], { stdio: "ignore", detached: true, windowsHide: true });
13799
+ const child = spawn3(file, [...args], {
13800
+ env: childEnvironment(process.env),
13801
+ stdio: "ignore",
13802
+ detached: true,
13803
+ windowsHide: true
13804
+ });
13508
13805
  child.once("error", () => settle(false));
13509
13806
  child.once("exit", (code) => settle(code === 0));
13510
13807
  child.unref();