@integrity-labs/agt-cli 0.27.144 → 0.27.146
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/agt.js +3 -3
- package/dist/{chunk-HPUIWWKD.js → chunk-3LRQ45BZ.js} +9 -11
- package/dist/{chunk-HPUIWWKD.js.map → chunk-3LRQ45BZ.js.map} +1 -1
- package/dist/{chunk-7GKJZBTB.js → chunk-J2HPXKP5.js} +208 -51
- package/dist/chunk-J2HPXKP5.js.map +1 -0
- package/dist/{claude-pair-runtime-GIUCD7IG.js → claude-pair-runtime-EZ4HC6D7.js} +2 -2
- package/dist/lib/manager-worker.js +67 -35
- package/dist/lib/manager-worker.js.map +1 -1
- package/dist/mcp/augmented-admin.js +21335 -0
- package/dist/mcp/slack-channel.js +151 -110
- package/dist/mcp/teams-channel.js +73 -29
- package/dist/mcp/telegram-channel.js +163 -80
- package/dist/{persistent-session-35PWSTLO.js → persistent-session-EUOWPJPS.js} +2 -3
- package/dist/{responsiveness-probe-MA4M2QM4.js → responsiveness-probe-IU3ALQYB.js} +2 -3
- package/dist/{responsiveness-probe-MA4M2QM4.js.map → responsiveness-probe-IU3ALQYB.js.map} +1 -1
- package/package.json +3 -2
- package/dist/chunk-354FAVQR.js +0 -173
- package/dist/chunk-354FAVQR.js.map +0 -1
- package/dist/chunk-7GKJZBTB.js.map +0 -1
- package/dist/daily-session-PNQX5URX.js +0 -27
- package/dist/persistent-session-35PWSTLO.js.map +0 -1
- /package/dist/{claude-pair-runtime-GIUCD7IG.js.map → claude-pair-runtime-EZ4HC6D7.js.map} +0 -0
- /package/dist/{daily-session-PNQX5URX.js.map → persistent-session-EUOWPJPS.js.map} +0 -0
|
@@ -14813,6 +14813,61 @@ function parsePeerAgentModeEnv(raw) {
|
|
|
14813
14813
|
return "off";
|
|
14814
14814
|
}
|
|
14815
14815
|
|
|
14816
|
+
// src/sender-policy-decline.ts
|
|
14817
|
+
function politeDeclineCopy(reason) {
|
|
14818
|
+
switch (reason) {
|
|
14819
|
+
case "internal_only":
|
|
14820
|
+
return "Sorry, I can only respond to people inside our organisation. This conversation appears to be from outside.";
|
|
14821
|
+
case "mode_manager_only":
|
|
14822
|
+
return "Sorry, I only respond to my manager in this channel.";
|
|
14823
|
+
case "mode_team_only":
|
|
14824
|
+
return "Sorry, this agent only replies to its team members.";
|
|
14825
|
+
case "mode_team_agents_only":
|
|
14826
|
+
return "Sorry, I only respond to agents on my team in this channel.";
|
|
14827
|
+
case "mode_agents_only":
|
|
14828
|
+
return "Sorry, I only respond to other agents in this channel.";
|
|
14829
|
+
}
|
|
14830
|
+
}
|
|
14831
|
+
|
|
14832
|
+
// src/inbound-access.ts
|
|
14833
|
+
function decideInboundAccess(input) {
|
|
14834
|
+
if (!input.content.forward) {
|
|
14835
|
+
return { kind: "drop", reason: `content:${input.content.reason}` };
|
|
14836
|
+
}
|
|
14837
|
+
if (input.isSelf) {
|
|
14838
|
+
return { kind: "drop", reason: "self" };
|
|
14839
|
+
}
|
|
14840
|
+
if (input.orgBoundary && !input.orgBoundary.forward) {
|
|
14841
|
+
return { kind: "drop", reason: "org_boundary" };
|
|
14842
|
+
}
|
|
14843
|
+
if (input.senderPolicy && !input.senderPolicy.forward) {
|
|
14844
|
+
const reason = input.senderPolicy.reason;
|
|
14845
|
+
const declineWorthy = input.sameOrg && reason !== "internal_only";
|
|
14846
|
+
return {
|
|
14847
|
+
kind: "drop",
|
|
14848
|
+
reason: `sender_policy:${reason}`,
|
|
14849
|
+
decline: declineWorthy ? politeDeclineCopy(reason) : void 0
|
|
14850
|
+
};
|
|
14851
|
+
}
|
|
14852
|
+
if (input.command && !input.isBot) {
|
|
14853
|
+
return {
|
|
14854
|
+
kind: "command",
|
|
14855
|
+
command: input.command.command,
|
|
14856
|
+
authorized: input.command.authorized
|
|
14857
|
+
};
|
|
14858
|
+
}
|
|
14859
|
+
if (input.isBot) {
|
|
14860
|
+
const peer = input.peer;
|
|
14861
|
+
if (!peer || peer.kind === "self") {
|
|
14862
|
+
return { kind: "drop", reason: peer?.kind === "self" ? "self" : "peer:unclassified" };
|
|
14863
|
+
}
|
|
14864
|
+
if (peer.kind === "drop") {
|
|
14865
|
+
return { kind: "drop", reason: `peer:${peer.reason ?? "unspecified"}` };
|
|
14866
|
+
}
|
|
14867
|
+
}
|
|
14868
|
+
return { kind: "admit" };
|
|
14869
|
+
}
|
|
14870
|
+
|
|
14816
14871
|
// src/telegram-peer-rate-limiter.ts
|
|
14817
14872
|
var SECOND_MS = 1e3;
|
|
14818
14873
|
var MINUTE_MS = 60 * SECOND_MS;
|
|
@@ -18108,14 +18163,68 @@ async function pollLoop() {
|
|
|
18108
18163
|
const content = typeof msg.text === "string" && msg.text.length > 0 ? msg.text : typeof msg.caption === "string" && msg.caption.length > 0 ? msg.caption : "";
|
|
18109
18164
|
if (content.length === 0 && classifiedAttachments.length === 0) continue;
|
|
18110
18165
|
const chatId = String(msg.chat.id);
|
|
18111
|
-
|
|
18166
|
+
const trimmedContent = content.trim();
|
|
18167
|
+
const isFromBot = !!msg.from?.is_bot;
|
|
18168
|
+
const nowMs = Date.now();
|
|
18169
|
+
const telegramCommand = isHelpSyntax(trimmedContent) ? "help" : isStatusSyntax(trimmedContent) ? "status" : isRestartSyntax(trimmedContent) ? "restart" : isInvestigateSyntax(trimmedContent) ? "investigate" : void 0;
|
|
18170
|
+
let classification;
|
|
18171
|
+
let peerLeaf;
|
|
18172
|
+
if (isFromBot) {
|
|
18173
|
+
if (PEER_DISABLED_GLOBAL) {
|
|
18174
|
+
peerLeaf = { kind: "drop", reason: "peer_disabled_global" };
|
|
18175
|
+
} else {
|
|
18176
|
+
const classifierMsg = {
|
|
18177
|
+
message_id: msg.message_id,
|
|
18178
|
+
text: msg.text,
|
|
18179
|
+
caption: msg.caption,
|
|
18180
|
+
from: msg.from ? { id: msg.from.id, username: msg.from.username, is_bot: msg.from.is_bot } : void 0,
|
|
18181
|
+
chat: { id: msg.chat.id, type: msg.chat.type },
|
|
18182
|
+
reply_to_message: msg.reply_to_message ? {
|
|
18183
|
+
message_id: msg.reply_to_message.message_id,
|
|
18184
|
+
from: msg.reply_to_message.from ? { id: msg.reply_to_message.from.id, is_bot: msg.reply_to_message.from.is_bot } : void 0
|
|
18185
|
+
} : void 0,
|
|
18186
|
+
entities: msg.entities,
|
|
18187
|
+
caption_entities: msg.caption_entities
|
|
18188
|
+
};
|
|
18189
|
+
const self = await resolveBotIdentity();
|
|
18190
|
+
classification = classifyPeerMessage(classifierMsg, PEER_CLASSIFIER_CONFIG, self);
|
|
18191
|
+
peerLeaf = {
|
|
18192
|
+
kind: classification.kind,
|
|
18193
|
+
reason: classification.kind === "drop" ? classification.reason : void 0
|
|
18194
|
+
};
|
|
18195
|
+
}
|
|
18196
|
+
}
|
|
18197
|
+
const access = decideInboundAccess({
|
|
18198
|
+
content: { forward: true },
|
|
18199
|
+
// empty content already filtered above
|
|
18200
|
+
// CodeRabbit: derive self from the peer classification so an echo of
|
|
18201
|
+
// our own outbound is dropped at the self step — BEFORE command
|
|
18202
|
+
// routing — instead of slipping through as a command. (Slack catches
|
|
18203
|
+
// self at evt.user===botUserId and Teams inside its activity filter;
|
|
18204
|
+
// Telegram's only self signal is the peer classifier.)
|
|
18205
|
+
isSelf: peerLeaf?.kind === "self",
|
|
18206
|
+
isBot: isFromBot,
|
|
18207
|
+
peer: peerLeaf,
|
|
18208
|
+
// TELEGRAM_ALLOWED_CHATS is Telegram's org-boundary axis (the drift
|
|
18209
|
+
// the consolidation names). An unlisted chat is external ⇒ silent.
|
|
18210
|
+
orgBoundary: ALLOWED_CHATS.size > 0 && !ALLOWED_CHATS.has(chatId) ? { forward: false } : { forward: true },
|
|
18211
|
+
command: telegramCommand ? { command: telegramCommand, authorized: true } : void 0,
|
|
18212
|
+
// No sender-policy mode system on Telegram, so no same-org decline.
|
|
18213
|
+
sameOrg: false
|
|
18214
|
+
});
|
|
18215
|
+
if (access.kind === "drop" && (access.reason === "org_boundary" || access.reason.startsWith("content:"))) {
|
|
18216
|
+
process.stderr.write(
|
|
18217
|
+
`telegram-channel(${AGENT_CODE_NAME}): inbound drop reason=${access.reason} chat=${redactId(chatId)}
|
|
18218
|
+
`
|
|
18219
|
+
);
|
|
18220
|
+
continue;
|
|
18221
|
+
}
|
|
18112
18222
|
observedChatClient?.observe({
|
|
18113
18223
|
chat_id: chatId,
|
|
18114
18224
|
chat_title: msg.chat.title ?? msg.chat.username ?? msg.chat.first_name ?? null,
|
|
18115
18225
|
chat_type: msg.chat.type ?? null
|
|
18116
18226
|
});
|
|
18117
|
-
|
|
18118
|
-
if (isHelpSyntax(trimmedContent)) {
|
|
18227
|
+
if (access.kind === "command" && access.command === "help") {
|
|
18119
18228
|
const disposition = await classifyRestartCommand(trimmedContent.replace(/^\/help/, "/restart"));
|
|
18120
18229
|
if (disposition === "act") {
|
|
18121
18230
|
await handleHelpCommand({
|
|
@@ -18125,7 +18234,7 @@ async function pollLoop() {
|
|
|
18125
18234
|
}
|
|
18126
18235
|
continue;
|
|
18127
18236
|
}
|
|
18128
|
-
if (
|
|
18237
|
+
if (access.kind === "command" && access.command === "status") {
|
|
18129
18238
|
const disposition = await classifyRestartCommand(
|
|
18130
18239
|
trimmedContent.replace(/^\/status/, "/restart")
|
|
18131
18240
|
);
|
|
@@ -18137,7 +18246,7 @@ async function pollLoop() {
|
|
|
18137
18246
|
}
|
|
18138
18247
|
continue;
|
|
18139
18248
|
}
|
|
18140
|
-
if (
|
|
18249
|
+
if (access.kind === "command" && access.command === "restart") {
|
|
18141
18250
|
const disposition = await classifyRestartCommand(trimmedContent);
|
|
18142
18251
|
if (disposition === "act") {
|
|
18143
18252
|
await handleRestartCommand({
|
|
@@ -18166,7 +18275,7 @@ async function pollLoop() {
|
|
|
18166
18275
|
}
|
|
18167
18276
|
continue;
|
|
18168
18277
|
}
|
|
18169
|
-
if (
|
|
18278
|
+
if (access.kind === "command" && access.command === "investigate") {
|
|
18170
18279
|
const disposition = await classifyInvestigateCommand(trimmedContent);
|
|
18171
18280
|
if (disposition === "act") {
|
|
18172
18281
|
await handleInvestigateCommand({
|
|
@@ -18197,8 +18306,6 @@ async function pollLoop() {
|
|
|
18197
18306
|
}
|
|
18198
18307
|
const userId = msg.from?.id != null ? String(msg.from.id) : "unknown";
|
|
18199
18308
|
const userName = msg.from?.username || [msg.from?.first_name, msg.from?.last_name].filter(Boolean).join(" ").trim() || userId;
|
|
18200
|
-
const isFromBot = !!msg.from?.is_bot;
|
|
18201
|
-
const nowMs = Date.now();
|
|
18202
18309
|
if (chatId && !isFromBot) {
|
|
18203
18310
|
resetThread(chatId, "");
|
|
18204
18311
|
}
|
|
@@ -18207,38 +18314,14 @@ async function pollLoop() {
|
|
|
18207
18314
|
peerRateLimiter.recordInboundHuman(chatId, nowMs);
|
|
18208
18315
|
}
|
|
18209
18316
|
let peerAgentMeta = null;
|
|
18210
|
-
if (
|
|
18211
|
-
if (
|
|
18317
|
+
if (access.kind === "drop") {
|
|
18318
|
+
if (access.reason !== "self") {
|
|
18319
|
+
const peerReason = access.reason.startsWith("peer:") ? access.reason.slice("peer:".length) : access.reason;
|
|
18212
18320
|
process.stderr.write(
|
|
18213
|
-
`telegram-channel(${AGENT_CODE_NAME}): peer_drop reason
|
|
18321
|
+
`telegram-channel(${AGENT_CODE_NAME}): peer_drop reason=${peerReason} chat=${redactId(chatId)} from=${redactId(String(msg.from?.id ?? "unknown"))}
|
|
18214
18322
|
`
|
|
18215
18323
|
);
|
|
18216
|
-
|
|
18217
|
-
}
|
|
18218
|
-
const classifierMsg = {
|
|
18219
|
-
message_id: msg.message_id,
|
|
18220
|
-
text: msg.text,
|
|
18221
|
-
caption: msg.caption,
|
|
18222
|
-
from: msg.from ? { id: msg.from.id, username: msg.from.username, is_bot: msg.from.is_bot } : void 0,
|
|
18223
|
-
chat: { id: msg.chat.id, type: msg.chat.type },
|
|
18224
|
-
reply_to_message: msg.reply_to_message ? {
|
|
18225
|
-
message_id: msg.reply_to_message.message_id,
|
|
18226
|
-
from: msg.reply_to_message.from ? { id: msg.reply_to_message.from.id, is_bot: msg.reply_to_message.from.is_bot } : void 0
|
|
18227
|
-
} : void 0,
|
|
18228
|
-
entities: msg.entities,
|
|
18229
|
-
caption_entities: msg.caption_entities
|
|
18230
|
-
};
|
|
18231
|
-
const self = await resolveBotIdentity();
|
|
18232
|
-
const classification = classifyPeerMessage(classifierMsg, PEER_CLASSIFIER_CONFIG, self);
|
|
18233
|
-
if (classification.kind === "self") {
|
|
18234
|
-
continue;
|
|
18235
|
-
}
|
|
18236
|
-
if (classification.kind === "drop") {
|
|
18237
|
-
process.stderr.write(
|
|
18238
|
-
`telegram-channel(${AGENT_CODE_NAME}): peer_drop reason=${classification.reason} chat=${redactId(chatId)} from=${redactId(String(msg.from?.id ?? "unknown"))}
|
|
18239
|
-
`
|
|
18240
|
-
);
|
|
18241
|
-
if (classification.reason === "cross_team_grant_missing") {
|
|
18324
|
+
if (classification?.kind === "drop" && classification.reason === "cross_team_grant_missing") {
|
|
18242
18325
|
crossTeamPeerAuditClient?.emit({
|
|
18243
18326
|
event_type: "telegram.peer.drop.cross_team_grant_missing",
|
|
18244
18327
|
peer_agent_id: null,
|
|
@@ -18248,52 +18331,52 @@ async function pollLoop() {
|
|
|
18248
18331
|
message_id: String(msg.message_id)
|
|
18249
18332
|
});
|
|
18250
18333
|
}
|
|
18251
|
-
continue;
|
|
18252
18334
|
}
|
|
18253
|
-
|
|
18254
|
-
|
|
18255
|
-
|
|
18256
|
-
|
|
18257
|
-
|
|
18258
|
-
|
|
18259
|
-
|
|
18260
|
-
|
|
18261
|
-
|
|
18335
|
+
continue;
|
|
18336
|
+
}
|
|
18337
|
+
if (isFromBot && classification?.kind === "peer-ingress") {
|
|
18338
|
+
const limit = await peerRateLimiter.recordInboundPeer(
|
|
18339
|
+
chatId,
|
|
18340
|
+
classification.peer.bot_id,
|
|
18341
|
+
nowMs
|
|
18342
|
+
);
|
|
18343
|
+
if (limit.decision !== "ok") {
|
|
18344
|
+
process.stderr.write(
|
|
18345
|
+
`telegram-channel(${AGENT_CODE_NAME}): peer_drop reason=${limit.decision} chat=${redactId(chatId)} from=${redactId(String(msg.from?.id ?? "unknown"))}
|
|
18262
18346
|
`
|
|
18263
|
-
|
|
18264
|
-
|
|
18265
|
-
|
|
18266
|
-
|
|
18267
|
-
|
|
18268
|
-
|
|
18269
|
-
|
|
18347
|
+
);
|
|
18348
|
+
continue;
|
|
18349
|
+
}
|
|
18350
|
+
if (limit.dailyBudgetWarn && !dailyBudgetWarned) {
|
|
18351
|
+
dailyBudgetWarned = true;
|
|
18352
|
+
process.stderr.write(
|
|
18353
|
+
`telegram-channel(${AGENT_CODE_NAME}): peer_warn reason=daily_budget_80pct
|
|
18270
18354
|
`
|
|
18271
|
-
|
|
18272
|
-
|
|
18273
|
-
|
|
18274
|
-
|
|
18275
|
-
|
|
18276
|
-
|
|
18277
|
-
|
|
18278
|
-
|
|
18279
|
-
|
|
18280
|
-
|
|
18281
|
-
|
|
18282
|
-
|
|
18283
|
-
|
|
18284
|
-
|
|
18285
|
-
|
|
18286
|
-
|
|
18287
|
-
|
|
18288
|
-
|
|
18289
|
-
|
|
18290
|
-
|
|
18291
|
-
|
|
18292
|
-
|
|
18293
|
-
|
|
18294
|
-
|
|
18295
|
-
|
|
18296
|
-
}
|
|
18355
|
+
);
|
|
18356
|
+
}
|
|
18357
|
+
peerAgentMeta = {
|
|
18358
|
+
code_name: classification.peer.code_name,
|
|
18359
|
+
agent_id: classification.peer.agent_id
|
|
18360
|
+
};
|
|
18361
|
+
const gate = classification.peer.gate_path;
|
|
18362
|
+
if (gate && gate !== "same_team" && gate !== "intra_org_unrestricted" && gate.startsWith("grant:")) {
|
|
18363
|
+
crossTeamPeerAuditClient?.emit({
|
|
18364
|
+
event_type: "telegram.peer.ingress.cross_team",
|
|
18365
|
+
peer_agent_id: classification.peer.agent_id || null,
|
|
18366
|
+
gate_path: gate,
|
|
18367
|
+
grant_id: gate.slice("grant:".length),
|
|
18368
|
+
chat_id: String(chatId),
|
|
18369
|
+
message_id: String(msg.message_id)
|
|
18370
|
+
});
|
|
18371
|
+
} else if (gate === "intra_org_unrestricted") {
|
|
18372
|
+
crossTeamPeerAuditClient?.emit({
|
|
18373
|
+
event_type: "telegram.peer.ingress.cross_team",
|
|
18374
|
+
peer_agent_id: classification.peer.agent_id || null,
|
|
18375
|
+
gate_path: gate,
|
|
18376
|
+
grant_id: null,
|
|
18377
|
+
chat_id: String(chatId),
|
|
18378
|
+
message_id: String(msg.message_id)
|
|
18379
|
+
});
|
|
18297
18380
|
}
|
|
18298
18381
|
}
|
|
18299
18382
|
const messageId = String(msg.message_id);
|
|
@@ -25,9 +25,8 @@ import {
|
|
|
25
25
|
takeAcpxExecFailureCount,
|
|
26
26
|
takeZombieDetection,
|
|
27
27
|
writePersistentClaudeWrapper
|
|
28
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-J2HPXKP5.js";
|
|
29
29
|
import "./chunk-WOOYOAPG.js";
|
|
30
|
-
import "./chunk-354FAVQR.js";
|
|
31
30
|
import "./chunk-XWVM4KPK.js";
|
|
32
31
|
export {
|
|
33
32
|
SEND_KEYS_ENTER_DELAY_MS,
|
|
@@ -57,4 +56,4 @@ export {
|
|
|
57
56
|
takeZombieDetection,
|
|
58
57
|
writePersistentClaudeWrapper
|
|
59
58
|
};
|
|
60
|
-
//# sourceMappingURL=persistent-session-
|
|
59
|
+
//# sourceMappingURL=persistent-session-EUOWPJPS.js.map
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
paneLogPath
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-J2HPXKP5.js";
|
|
4
4
|
import "./chunk-WOOYOAPG.js";
|
|
5
|
-
import "./chunk-354FAVQR.js";
|
|
6
5
|
import "./chunk-XWVM4KPK.js";
|
|
7
6
|
|
|
8
7
|
// src/lib/responsiveness-probe.ts
|
|
@@ -155,4 +154,4 @@ export {
|
|
|
155
154
|
livePendingInboundOldestAgeSeconds,
|
|
156
155
|
oldestLivePendingInboundMtimeMs
|
|
157
156
|
};
|
|
158
|
-
//# sourceMappingURL=responsiveness-probe-
|
|
157
|
+
//# sourceMappingURL=responsiveness-probe-IU3ALQYB.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/lib/responsiveness-probe.ts"],"sourcesContent":["/**\n * ENG-5399 — Tier 1 responsiveness probe (manager-side).\n *\n * Cheap, fast-cadence canary that catches \"agent went silent\" inside\n * minutes, well before the existing synthetic-probe cron's ~35 min\n * staleness window (`SyntheticReplyAgeSeconds`, ENG-5122).\n *\n * Mechanism: for each managed agent, read the mtime of the agent's\n * `pane.log` and report `now - mtime` as `PaneActivityAgeSeconds` via\n * a new `/host/responsiveness-probe` endpoint. `pane.log` is the\n * tmux pipe-pane sink set up by `setupPaneLog()` — any visible\n * activity (assistant turns, tool calls, in-place progress\n * heartbeats) bumps its mtime. A silent agent has a steadily\n * climbing age that lands in CloudWatch and trips a per-agent alarm.\n *\n * ENG-6017 adds a second per-agent signal on the same cadence:\n * `pending_inbound_oldest_age_seconds` — the age of the oldest marker\n * file across the agent's `*-pending-inbound/` directories (written by\n * the channel MCP servers for inbounds awaiting delivery). This is the\n * one artifact of the \"message typed but never submitted\" failure mode\n * that every other canary is blind to: in the koda incident\n * (2026-06-04) an operator Slack DM sat undelivered for 40+ minutes\n * while pane-activity stayed fresh (health checks), synthetic probes\n * were answered by the one-shot fallback, and heartbeat/session-alive\n * only reflect manager health. The field is OMITTED (not zero) when the\n * agent has no pending-inbound markers — the API treats absent as\n * \"no signal\", never as \"healthy\" (absent-vs-zero matters for\n * mixed-version fleets where old CLIs don't report it at all).\n *\n * Run from `pollCycle()` in `manager-worker.ts` on a configurable\n * interval (default 5 min via `AUGMENTED_RESPONSIVENESS_INTERVAL_MS`).\n */\n\nimport { mkdirSync, readdirSync, readFileSync, renameSync, statSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { paneLogPath } from './persistent-session.js';\n\nexport interface ResponsivenessProbeResult {\n code_name: string;\n pane_activity_age_seconds: number;\n /**\n * ENG-6017: age (s) of the oldest marker file across the agent's\n * `*-pending-inbound/` directories. Omitted when no markers exist —\n * absent means \"no signal\", NOT \"zero / healthy\".\n */\n pending_inbound_oldest_age_seconds?: number;\n}\n\nconst DEFAULT_INTERVAL_MS = 5 * 60 * 1000;\n\nexport function getResponsivenessIntervalMs(): number {\n const raw = process.env.AUGMENTED_RESPONSIVENESS_INTERVAL_MS;\n if (!raw) return DEFAULT_INTERVAL_MS;\n const parsed = Number.parseInt(raw, 10);\n return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_INTERVAL_MS;\n}\n\n/**\n * ENG-6017: oldest pending-inbound marker mtime (ms epoch) for an agent,\n * or null when the agent has no markers / no pending-inbound dirs.\n *\n * The channel MCP servers (slack-channel, telegram-channel, …) write one\n * marker file per inbound into `~/.augmented/<codeName>/<channel>-pending-\n * inbound/` and clear it when the agent acknowledges the message. The\n * directory layout is the contract here — read-only, no IPC with the MCP\n * (the MCP and CLI release independently; file mtimes need no protocol).\n *\n * ENG-6072: only plain, non-hidden files count as markers. The msteams MCP\n * keeps `.markers/` and `.processed/` housekeeping SUBDIRECTORIES inside its\n * pending-inbound dir; their mtimes never advance, so statting every dirent\n * made the gauge climb forever and fired pending-inbound-stale on agents with\n * zero stranded messages (kylie ~3.4d / scout ~34h false ALARMs the moment\n * ENG-6023 activated the alarm). Dot-entries are skipped wholesale — the\n * hidden namespace is reserved for MCP bookkeeping, never for markers.\n */\nfunction oldestPendingInboundMtimeMs(agentHomeDir: string): number | null {\n let oldest: number | null = null;\n let entries;\n try {\n entries = readdirSync(agentHomeDir, { withFileTypes: true });\n } catch {\n return null; // agent home missing — nothing to report\n }\n for (const entry of entries) {\n if (!entry.isDirectory() || !entry.name.endsWith('-pending-inbound')) continue;\n const dir = join(agentHomeDir, entry.name);\n let files;\n try {\n files = readdirSync(dir, { withFileTypes: true });\n } catch {\n continue;\n }\n for (const file of files) {\n if (!file.isFile() || file.name.startsWith('.')) continue;\n try {\n const mtimeMs = statSync(join(dir, file.name)).mtimeMs;\n if (oldest === null || mtimeMs < oldest) oldest = mtimeMs;\n } catch {\n // Marker drained between readdir and stat — that's the happy path.\n }\n }\n }\n return oldest;\n}\n\n/**\n * ENG-6160: classify a marker file for the LIVE-inbound scan.\n * - `true` → flagged `\"undeliverable\": true` (dead-letter, exclude).\n * - `null` → vanished mid-scan (ENOENT) — drained between stat and read, the\n * happy path; exclude it rather than count an already-gone file.\n * - `false` → still present but malformed / unreadable for another reason —\n * treated as LIVE so a corrupt marker can never mask a real wedge.\n */\nfunction isUndeliverableMarker(markerPath: string): boolean | null {\n try {\n const parsed = JSON.parse(readFileSync(markerPath, 'utf8')) as { undeliverable?: unknown };\n return parsed?.undeliverable === true;\n } catch (error) {\n return (error as NodeJS.ErrnoException).code === 'ENOENT' ? null : false;\n }\n}\n\n/**\n * ENG-6160: oldest *LIVE* pending-inbound marker mtime (ms epoch) for an agent,\n * or null when there is no live marker. \"Live\" excludes:\n *\n * - markers older than `sessionStartMs` — a marker written before the current\n * session started is a leftover from a PREVIOUS session and cannot mean\n * *this* session is failing to drain. This is the load-bearing exclusion:\n * without it, an orphan marker survives a fresh respawn and the wedge\n * detector re-fires forever on a healthy idle agent (the sherlock enforce\n * loop, 2026-06-08: `inboundAge=3389s` on a `● Ready.` session).\n * - markers flagged `undeliverable: true` — already dead-lettered by the channel.\n *\n * Distinct from `oldestPendingInboundMtimeMs` (which counts ALL markers and\n * feeds the ENG-6017 `pending-inbound-stale` CloudWatch alarm — that alarm\n * *wants* to fire on a stuck inbound, so its semantics must NOT change). This\n * variant is wedge-detection-only.\n */\nexport function oldestLivePendingInboundMtimeMs(\n agentHomeDir: string,\n opts: { sessionStartMs?: number | null } = {},\n): number | null {\n const sessionStartMs = opts.sessionStartMs ?? null;\n let oldest: number | null = null;\n let entries;\n try {\n entries = readdirSync(agentHomeDir, { withFileTypes: true });\n } catch {\n return null;\n }\n for (const entry of entries) {\n if (!entry.isDirectory() || !entry.name.endsWith('-pending-inbound')) continue;\n const dir = join(agentHomeDir, entry.name);\n let files;\n try {\n files = readdirSync(dir, { withFileTypes: true });\n } catch {\n continue;\n }\n for (const file of files) {\n if (!file.isFile() || file.name.startsWith('.')) continue;\n const full = join(dir, file.name);\n let mtimeMs: number;\n try {\n mtimeMs = statSync(full).mtimeMs;\n } catch {\n continue; // drained between readdir and stat — happy path\n }\n if (sessionStartMs !== null && mtimeMs < sessionStartMs) continue; // pre-session leftover\n const undeliverable = isUndeliverableMarker(full);\n if (undeliverable === null) continue; // vanished between stat and read — drained, exclude\n if (undeliverable) continue; // already dead-lettered\n if (oldest === null || mtimeMs < oldest) oldest = mtimeMs;\n }\n }\n return oldest;\n}\n\n/**\n * ENG-6160: age (s) of the oldest LIVE pending-inbound marker for an agent, or\n * null when none. The wedge detector uses this instead of the alarm-facing\n * `pending_inbound_oldest_age_seconds` so a stale/dead-letter marker can't\n * false-fire a respawn.\n */\nexport function livePendingInboundOldestAgeSeconds(\n codeName: string,\n sessionStartMs: number | null,\n now: Date = new Date(),\n): number | null {\n const oldest = oldestLivePendingInboundMtimeMs(dirname(paneLogPath(codeName)), { sessionStartMs });\n if (oldest === null) return null;\n return Math.max(0, Math.floor((now.getTime() - oldest) / 1000));\n}\n\n/**\n * ENG-6160: move every pending-inbound marker for an agent aside into a sibling\n * `<channel>-pending-inbound-stale/` directory (NOT silently deleted — the\n * payload pointer is preserved for forensics), returning the count moved.\n *\n * Called on a force-fresh wedge respawn: the markers belonged to the wedged\n * session that is being torn down; the fresh session cannot meaningfully\n * process a stale, out-of-context message, and leaving them on disk both keeps\n * the ENG-6017 alarm lit and (pre-ENG-6160) re-fed the wedge loop. The stale\n * dir does not end in `-pending-inbound`, so neither the probe nor this scan\n * re-counts moved markers.\n */\nexport function deadLetterPendingInbound(codeName: string, _now: Date = new Date()): number {\n const home = dirname(paneLogPath(codeName));\n let moved = 0;\n let entries;\n try {\n entries = readdirSync(home, { withFileTypes: true });\n } catch {\n return 0;\n }\n for (const entry of entries) {\n if (!entry.isDirectory() || !entry.name.endsWith('-pending-inbound')) continue;\n const dir = join(home, entry.name);\n const deadDir = join(home, `${entry.name}-stale`);\n let files;\n try {\n files = readdirSync(dir, { withFileTypes: true });\n } catch {\n continue;\n }\n for (const file of files) {\n if (!file.isFile() || file.name.startsWith('.')) continue;\n try {\n mkdirSync(deadDir, { recursive: true });\n renameSync(join(dir, file.name), join(deadDir, file.name));\n moved++;\n } catch {\n // best-effort — a marker that vanished or can't move is left as-is\n }\n }\n }\n return moved;\n}\n\n/**\n * Compute the pane.log age for each agent. Missing or unreadable\n * pane.log returns null — the caller should drop those entries\n * rather than fabricate a \"fresh\" or \"ancient\" value. A missing\n * file means the agent has never spawned in this manager generation,\n * which is a separate problem covered by SessionAliveAgeSeconds.\n */\nexport function collectResponsivenessProbes(\n codeNames: string[],\n now: Date = new Date(),\n): ResponsivenessProbeResult[] {\n const nowMs = now.getTime();\n const results: ResponsivenessProbeResult[] = [];\n for (const codeName of codeNames) {\n try {\n const panePath = paneLogPath(codeName);\n const mtimeMs = statSync(panePath).mtimeMs;\n const ageSeconds = Math.max(0, Math.floor((nowMs - mtimeMs) / 1000));\n const result: ResponsivenessProbeResult = {\n code_name: codeName,\n pane_activity_age_seconds: ageSeconds,\n };\n // ENG-6017: piggyback the pending-inbound drain-age scan on the same\n // cadence. Field omitted (not 0) when there are no markers.\n const oldestMarkerMs = oldestPendingInboundMtimeMs(dirname(panePath));\n if (oldestMarkerMs !== null) {\n result.pending_inbound_oldest_age_seconds = Math.max(\n 0,\n Math.floor((nowMs - oldestMarkerMs) / 1000),\n );\n }\n results.push(result);\n } catch {\n // No pane.log yet (fresh agent, never spawned) — skip. The\n // session-alive monitor already covers the \"should be running\n // but isn't\" case.\n }\n }\n return results;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/lib/responsiveness-probe.ts"],"sourcesContent":["/**\n * ENG-5399 — Tier 1 responsiveness probe (manager-side).\n *\n * Cheap, fast-cadence canary that catches \"agent went silent\" inside\n * minutes, well before the existing synthetic-probe cron's ~35 min\n * staleness window (`SyntheticReplyAgeSeconds`, ENG-5122).\n *\n * Mechanism: for each managed agent, read the mtime of the agent's\n * `pane.log` and report `now - mtime` as `PaneActivityAgeSeconds` via\n * a new `/host/responsiveness-probe` endpoint. `pane.log` is the\n * tmux pipe-pane sink set up by `setupPaneLog()` — any visible\n * activity (assistant turns, tool calls, in-place progress\n * heartbeats) bumps its mtime. A silent agent has a steadily\n * climbing age that lands in CloudWatch and trips a per-agent alarm.\n *\n * ENG-6017 adds a second per-agent signal on the same cadence:\n * `pending_inbound_oldest_age_seconds` — the age of the oldest marker\n * file across the agent's `*-pending-inbound/` directories (written by\n * the channel MCP servers for inbounds awaiting delivery). This is the\n * one artifact of the \"message typed but never submitted\" failure mode\n * that every other canary is blind to: in the koda incident\n * (2026-06-04) an operator Slack DM sat undelivered for 40+ minutes\n * while pane-activity stayed fresh (health checks), synthetic probes\n * were answered by the one-shot fallback, and heartbeat/session-alive\n * only reflect manager health. The field is OMITTED (not zero) when the\n * agent has no pending-inbound markers — the API treats absent as\n * \"no signal\", never as \"healthy\" (absent-vs-zero matters for\n * mixed-version fleets where old CLIs don't report it at all).\n *\n * Run from `pollCycle()` in `manager-worker.ts` on a configurable\n * interval (default 5 min via `AUGMENTED_RESPONSIVENESS_INTERVAL_MS`).\n */\n\nimport { mkdirSync, readdirSync, readFileSync, renameSync, statSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { paneLogPath } from './persistent-session.js';\n\nexport interface ResponsivenessProbeResult {\n code_name: string;\n pane_activity_age_seconds: number;\n /**\n * ENG-6017: age (s) of the oldest marker file across the agent's\n * `*-pending-inbound/` directories. Omitted when no markers exist —\n * absent means \"no signal\", NOT \"zero / healthy\".\n */\n pending_inbound_oldest_age_seconds?: number;\n}\n\nconst DEFAULT_INTERVAL_MS = 5 * 60 * 1000;\n\nexport function getResponsivenessIntervalMs(): number {\n const raw = process.env.AUGMENTED_RESPONSIVENESS_INTERVAL_MS;\n if (!raw) return DEFAULT_INTERVAL_MS;\n const parsed = Number.parseInt(raw, 10);\n return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_INTERVAL_MS;\n}\n\n/**\n * ENG-6017: oldest pending-inbound marker mtime (ms epoch) for an agent,\n * or null when the agent has no markers / no pending-inbound dirs.\n *\n * The channel MCP servers (slack-channel, telegram-channel, …) write one\n * marker file per inbound into `~/.augmented/<codeName>/<channel>-pending-\n * inbound/` and clear it when the agent acknowledges the message. The\n * directory layout is the contract here — read-only, no IPC with the MCP\n * (the MCP and CLI release independently; file mtimes need no protocol).\n *\n * ENG-6072: only plain, non-hidden files count as markers. The msteams MCP\n * keeps `.markers/` and `.processed/` housekeeping SUBDIRECTORIES inside its\n * pending-inbound dir; their mtimes never advance, so statting every dirent\n * made the gauge climb forever and fired pending-inbound-stale on agents with\n * zero stranded messages (kylie ~3.4d / scout ~34h false ALARMs the moment\n * ENG-6023 activated the alarm). Dot-entries are skipped wholesale — the\n * hidden namespace is reserved for MCP bookkeeping, never for markers.\n */\nfunction oldestPendingInboundMtimeMs(agentHomeDir: string): number | null {\n let oldest: number | null = null;\n let entries;\n try {\n entries = readdirSync(agentHomeDir, { withFileTypes: true });\n } catch {\n return null; // agent home missing — nothing to report\n }\n for (const entry of entries) {\n if (!entry.isDirectory() || !entry.name.endsWith('-pending-inbound')) continue;\n const dir = join(agentHomeDir, entry.name);\n let files;\n try {\n files = readdirSync(dir, { withFileTypes: true });\n } catch {\n continue;\n }\n for (const file of files) {\n if (!file.isFile() || file.name.startsWith('.')) continue;\n try {\n const mtimeMs = statSync(join(dir, file.name)).mtimeMs;\n if (oldest === null || mtimeMs < oldest) oldest = mtimeMs;\n } catch {\n // Marker drained between readdir and stat — that's the happy path.\n }\n }\n }\n return oldest;\n}\n\n/**\n * ENG-6160: classify a marker file for the LIVE-inbound scan.\n * - `true` → flagged `\"undeliverable\": true` (dead-letter, exclude).\n * - `null` → vanished mid-scan (ENOENT) — drained between stat and read, the\n * happy path; exclude it rather than count an already-gone file.\n * - `false` → still present but malformed / unreadable for another reason —\n * treated as LIVE so a corrupt marker can never mask a real wedge.\n */\nfunction isUndeliverableMarker(markerPath: string): boolean | null {\n try {\n const parsed = JSON.parse(readFileSync(markerPath, 'utf8')) as { undeliverable?: unknown };\n return parsed?.undeliverable === true;\n } catch (error) {\n return (error as NodeJS.ErrnoException).code === 'ENOENT' ? null : false;\n }\n}\n\n/**\n * ENG-6160: oldest *LIVE* pending-inbound marker mtime (ms epoch) for an agent,\n * or null when there is no live marker. \"Live\" excludes:\n *\n * - markers older than `sessionStartMs` — a marker written before the current\n * session started is a leftover from a PREVIOUS session and cannot mean\n * *this* session is failing to drain. This is the load-bearing exclusion:\n * without it, an orphan marker survives a fresh respawn and the wedge\n * detector re-fires forever on a healthy idle agent (the sherlock enforce\n * loop, 2026-06-08: `inboundAge=3389s` on a `● Ready.` session).\n * - markers flagged `undeliverable: true` — already dead-lettered by the channel.\n *\n * Distinct from `oldestPendingInboundMtimeMs` (which counts ALL markers and\n * feeds the ENG-6017 `pending-inbound-stale` CloudWatch alarm — that alarm\n * *wants* to fire on a stuck inbound, so its semantics must NOT change). This\n * variant is wedge-detection-only.\n */\nexport function oldestLivePendingInboundMtimeMs(\n agentHomeDir: string,\n opts: { sessionStartMs?: number | null } = {},\n): number | null {\n const sessionStartMs = opts.sessionStartMs ?? null;\n let oldest: number | null = null;\n let entries;\n try {\n entries = readdirSync(agentHomeDir, { withFileTypes: true });\n } catch {\n return null;\n }\n for (const entry of entries) {\n if (!entry.isDirectory() || !entry.name.endsWith('-pending-inbound')) continue;\n const dir = join(agentHomeDir, entry.name);\n let files;\n try {\n files = readdirSync(dir, { withFileTypes: true });\n } catch {\n continue;\n }\n for (const file of files) {\n if (!file.isFile() || file.name.startsWith('.')) continue;\n const full = join(dir, file.name);\n let mtimeMs: number;\n try {\n mtimeMs = statSync(full).mtimeMs;\n } catch {\n continue; // drained between readdir and stat — happy path\n }\n if (sessionStartMs !== null && mtimeMs < sessionStartMs) continue; // pre-session leftover\n const undeliverable = isUndeliverableMarker(full);\n if (undeliverable === null) continue; // vanished between stat and read — drained, exclude\n if (undeliverable) continue; // already dead-lettered\n if (oldest === null || mtimeMs < oldest) oldest = mtimeMs;\n }\n }\n return oldest;\n}\n\n/**\n * ENG-6160: age (s) of the oldest LIVE pending-inbound marker for an agent, or\n * null when none. The wedge detector uses this instead of the alarm-facing\n * `pending_inbound_oldest_age_seconds` so a stale/dead-letter marker can't\n * false-fire a respawn.\n */\nexport function livePendingInboundOldestAgeSeconds(\n codeName: string,\n sessionStartMs: number | null,\n now: Date = new Date(),\n): number | null {\n const oldest = oldestLivePendingInboundMtimeMs(dirname(paneLogPath(codeName)), { sessionStartMs });\n if (oldest === null) return null;\n return Math.max(0, Math.floor((now.getTime() - oldest) / 1000));\n}\n\n/**\n * ENG-6160: move every pending-inbound marker for an agent aside into a sibling\n * `<channel>-pending-inbound-stale/` directory (NOT silently deleted — the\n * payload pointer is preserved for forensics), returning the count moved.\n *\n * Called on a force-fresh wedge respawn: the markers belonged to the wedged\n * session that is being torn down; the fresh session cannot meaningfully\n * process a stale, out-of-context message, and leaving them on disk both keeps\n * the ENG-6017 alarm lit and (pre-ENG-6160) re-fed the wedge loop. The stale\n * dir does not end in `-pending-inbound`, so neither the probe nor this scan\n * re-counts moved markers.\n */\nexport function deadLetterPendingInbound(codeName: string, _now: Date = new Date()): number {\n const home = dirname(paneLogPath(codeName));\n let moved = 0;\n let entries;\n try {\n entries = readdirSync(home, { withFileTypes: true });\n } catch {\n return 0;\n }\n for (const entry of entries) {\n if (!entry.isDirectory() || !entry.name.endsWith('-pending-inbound')) continue;\n const dir = join(home, entry.name);\n const deadDir = join(home, `${entry.name}-stale`);\n let files;\n try {\n files = readdirSync(dir, { withFileTypes: true });\n } catch {\n continue;\n }\n for (const file of files) {\n if (!file.isFile() || file.name.startsWith('.')) continue;\n try {\n mkdirSync(deadDir, { recursive: true });\n renameSync(join(dir, file.name), join(deadDir, file.name));\n moved++;\n } catch {\n // best-effort — a marker that vanished or can't move is left as-is\n }\n }\n }\n return moved;\n}\n\n/**\n * Compute the pane.log age for each agent. Missing or unreadable\n * pane.log returns null — the caller should drop those entries\n * rather than fabricate a \"fresh\" or \"ancient\" value. A missing\n * file means the agent has never spawned in this manager generation,\n * which is a separate problem covered by SessionAliveAgeSeconds.\n */\nexport function collectResponsivenessProbes(\n codeNames: string[],\n now: Date = new Date(),\n): ResponsivenessProbeResult[] {\n const nowMs = now.getTime();\n const results: ResponsivenessProbeResult[] = [];\n for (const codeName of codeNames) {\n try {\n const panePath = paneLogPath(codeName);\n const mtimeMs = statSync(panePath).mtimeMs;\n const ageSeconds = Math.max(0, Math.floor((nowMs - mtimeMs) / 1000));\n const result: ResponsivenessProbeResult = {\n code_name: codeName,\n pane_activity_age_seconds: ageSeconds,\n };\n // ENG-6017: piggyback the pending-inbound drain-age scan on the same\n // cadence. Field omitted (not 0) when there are no markers.\n const oldestMarkerMs = oldestPendingInboundMtimeMs(dirname(panePath));\n if (oldestMarkerMs !== null) {\n result.pending_inbound_oldest_age_seconds = Math.max(\n 0,\n Math.floor((nowMs - oldestMarkerMs) / 1000),\n );\n }\n results.push(result);\n } catch {\n // No pane.log yet (fresh agent, never spawned) — skip. The\n // session-alive monitor already covers the \"should be running\n // but isn't\" case.\n }\n }\n return results;\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@integrity-labs/agt-cli",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.146",
|
|
4
4
|
"description": "Augmented Team CLI — agent provisioning and management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
},
|
|
24
24
|
"scripts": {
|
|
25
25
|
"build": "tsup && npm run build:mcp-assets && npm run build:cli-assets",
|
|
26
|
-
"build:mcp-assets": "mkdir -p dist/mcp && cp ../../packages/mcp/dist/index.js dist/mcp/index.js && cp ../../packages/mcp/dist/slack-channel.js dist/mcp/slack-channel.js && cp ../../packages/mcp/dist/direct-chat-channel.js dist/mcp/direct-chat-channel.js && cp ../../packages/mcp/dist/telegram-channel.js dist/mcp/telegram-channel.js && cp ../../packages/mcp/dist/teams-channel.js dist/mcp/teams-channel.js",
|
|
26
|
+
"build:mcp-assets": "mkdir -p dist/mcp && cp ../../packages/mcp/dist/index.js dist/mcp/index.js && cp ../../packages/mcp/dist/slack-channel.js dist/mcp/slack-channel.js && cp ../../packages/mcp/dist/direct-chat-channel.js dist/mcp/direct-chat-channel.js && cp ../../packages/mcp/dist/telegram-channel.js dist/mcp/telegram-channel.js && cp ../../packages/mcp/dist/teams-channel.js dist/mcp/teams-channel.js && cp ../../packages/augmented-admin-mcp/dist/index.js dist/mcp/augmented-admin.js",
|
|
27
27
|
"build:cli-assets": "mkdir -p dist/assets && cp assets/impersonate-statusline.sh dist/assets/impersonate-statusline.sh && chmod +x dist/assets/impersonate-statusline.sh",
|
|
28
28
|
"dev": "tsx watch src/bin/agt.ts",
|
|
29
29
|
"test": "vitest run",
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@augmented/core": "workspace:*",
|
|
54
54
|
"@integrity-labs/augmented-mcp": "workspace:*",
|
|
55
|
+
"@integrity-labs/augmented-admin-mcp": "workspace:*",
|
|
55
56
|
"@types/node": "22.19.11",
|
|
56
57
|
"@types/react": "19.2.14",
|
|
57
58
|
"@types/ws": "8.18.1",
|
package/dist/chunk-354FAVQR.js
DELETED
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
// src/lib/daily-session.ts
|
|
2
|
-
import { randomUUID } from "crypto";
|
|
3
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "fs";
|
|
4
|
-
import { homedir } from "os";
|
|
5
|
-
import { join } from "path";
|
|
6
|
-
var HISTORY_DAYS = 7;
|
|
7
|
-
function profileDir(codeName) {
|
|
8
|
-
return join(homedir(), ".augmented", codeName);
|
|
9
|
-
}
|
|
10
|
-
function dailySessionPath(codeName) {
|
|
11
|
-
return join(profileDir(codeName), "daily-session.json");
|
|
12
|
-
}
|
|
13
|
-
function todayLocalIso(now = /* @__PURE__ */ new Date(), timezone) {
|
|
14
|
-
if (timezone) {
|
|
15
|
-
try {
|
|
16
|
-
const fmt = new Intl.DateTimeFormat("en-CA", {
|
|
17
|
-
timeZone: timezone,
|
|
18
|
-
year: "numeric",
|
|
19
|
-
month: "2-digit",
|
|
20
|
-
day: "2-digit"
|
|
21
|
-
});
|
|
22
|
-
return fmt.format(now);
|
|
23
|
-
} catch {
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
const y = now.getFullYear();
|
|
27
|
-
const m = String(now.getMonth() + 1).padStart(2, "0");
|
|
28
|
-
const d = String(now.getDate()).padStart(2, "0");
|
|
29
|
-
return `${y}-${m}-${d}`;
|
|
30
|
-
}
|
|
31
|
-
function readFile(codeName) {
|
|
32
|
-
const path = dailySessionPath(codeName);
|
|
33
|
-
if (!existsSync(path)) return { current: null, history: [] };
|
|
34
|
-
try {
|
|
35
|
-
const raw = readFileSync(path, "utf-8");
|
|
36
|
-
const parsed = JSON.parse(raw);
|
|
37
|
-
return {
|
|
38
|
-
current: parsed.current ?? null,
|
|
39
|
-
history: Array.isArray(parsed.history) ? parsed.history : []
|
|
40
|
-
};
|
|
41
|
-
} catch {
|
|
42
|
-
return { current: null, history: [] };
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
function writeFile(codeName, data) {
|
|
46
|
-
const dir = profileDir(codeName);
|
|
47
|
-
mkdirSync(dir, { recursive: true });
|
|
48
|
-
const finalPath = dailySessionPath(codeName);
|
|
49
|
-
const tmpPath = `${finalPath}.${process.pid}.${randomUUID()}.tmp`;
|
|
50
|
-
writeFileSync(tmpPath, JSON.stringify(data, null, 2), "utf-8");
|
|
51
|
-
renameSync(tmpPath, finalPath);
|
|
52
|
-
}
|
|
53
|
-
function trimHistory(history, now, timezone) {
|
|
54
|
-
const cutoff = new Date(now);
|
|
55
|
-
cutoff.setDate(cutoff.getDate() - HISTORY_DAYS);
|
|
56
|
-
const cutoffIso = todayLocalIso(cutoff, timezone);
|
|
57
|
-
return history.filter((h) => h.date >= cutoffIso).slice(0, HISTORY_DAYS);
|
|
58
|
-
}
|
|
59
|
-
function getOrCreateDailySession(codeName, now = /* @__PURE__ */ new Date(), timezone) {
|
|
60
|
-
const today = todayLocalIso(now, timezone);
|
|
61
|
-
const file = readFile(codeName);
|
|
62
|
-
if (file.current && file.current.date === today) {
|
|
63
|
-
return { sessionId: file.current.sessionId, isNew: false };
|
|
64
|
-
}
|
|
65
|
-
const next = {
|
|
66
|
-
date: today,
|
|
67
|
-
sessionId: randomUUID(),
|
|
68
|
-
startedAt: now.toISOString()
|
|
69
|
-
};
|
|
70
|
-
const history = trimHistory(
|
|
71
|
-
[...file.current ? [file.current] : [], ...file.history],
|
|
72
|
-
now,
|
|
73
|
-
timezone
|
|
74
|
-
);
|
|
75
|
-
writeFile(codeName, { current: next, history });
|
|
76
|
-
return { sessionId: next.sessionId, isNew: true };
|
|
77
|
-
}
|
|
78
|
-
function markDailySessionSpawn(codeName, sessionId, now = /* @__PURE__ */ new Date(), timezone) {
|
|
79
|
-
const today = todayLocalIso(now, timezone);
|
|
80
|
-
const file = readFile(codeName);
|
|
81
|
-
if (file.current && file.current.date === today && file.current.sessionId === sessionId) {
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
const next = {
|
|
85
|
-
date: today,
|
|
86
|
-
sessionId,
|
|
87
|
-
startedAt: now.toISOString()
|
|
88
|
-
};
|
|
89
|
-
const history = trimHistory(
|
|
90
|
-
[...file.current ? [file.current] : [], ...file.history],
|
|
91
|
-
now,
|
|
92
|
-
timezone
|
|
93
|
-
);
|
|
94
|
-
writeFile(codeName, { current: next, history });
|
|
95
|
-
}
|
|
96
|
-
function rotateDailySession(codeName, now = /* @__PURE__ */ new Date(), timezone) {
|
|
97
|
-
const today = todayLocalIso(now, timezone);
|
|
98
|
-
const file = readFile(codeName);
|
|
99
|
-
const next = {
|
|
100
|
-
date: today,
|
|
101
|
-
sessionId: randomUUID(),
|
|
102
|
-
startedAt: now.toISOString()
|
|
103
|
-
};
|
|
104
|
-
const history = trimHistory(
|
|
105
|
-
[...file.current ? [file.current] : [], ...file.history],
|
|
106
|
-
now,
|
|
107
|
-
timezone
|
|
108
|
-
);
|
|
109
|
-
writeFile(codeName, { current: next, history });
|
|
110
|
-
return next.sessionId;
|
|
111
|
-
}
|
|
112
|
-
function encodeProjectPath(projectDir) {
|
|
113
|
-
return "-" + projectDir.replace(/^\//, "").replace(/[/.]/g, "-");
|
|
114
|
-
}
|
|
115
|
-
function sessionFileExists(projectDir, sessionId) {
|
|
116
|
-
const path = join(
|
|
117
|
-
homedir(),
|
|
118
|
-
".claude",
|
|
119
|
-
"projects",
|
|
120
|
-
encodeProjectPath(projectDir),
|
|
121
|
-
`${sessionId}.jsonl`
|
|
122
|
-
);
|
|
123
|
-
return existsSync(path);
|
|
124
|
-
}
|
|
125
|
-
function sessionTranscriptDir(projectDir) {
|
|
126
|
-
return join(homedir(), ".claude", "projects", encodeProjectPath(projectDir));
|
|
127
|
-
}
|
|
128
|
-
function sessionFilePath(projectDir, sessionId) {
|
|
129
|
-
return join(sessionTranscriptDir(projectDir), `${sessionId}.jsonl`);
|
|
130
|
-
}
|
|
131
|
-
function transcriptActivityAgeSeconds(projectDir, sessionId, now = /* @__PURE__ */ new Date()) {
|
|
132
|
-
if (!sessionId) return null;
|
|
133
|
-
try {
|
|
134
|
-
const mtimeMs = statSync(sessionFilePath(projectDir, sessionId)).mtimeMs;
|
|
135
|
-
return Math.max(0, Math.floor((now.getTime() - mtimeMs) / 1e3));
|
|
136
|
-
} catch {
|
|
137
|
-
return null;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
function isAgentIdle(projectDir, sessionId, idleSeconds = 60, now = /* @__PURE__ */ new Date()) {
|
|
141
|
-
const path = sessionFilePath(projectDir, sessionId);
|
|
142
|
-
if (!existsSync(path)) return true;
|
|
143
|
-
try {
|
|
144
|
-
const mtimeMs = statSync(path).mtimeMs;
|
|
145
|
-
return now.getTime() - mtimeMs >= idleSeconds * 1e3;
|
|
146
|
-
} catch {
|
|
147
|
-
return false;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
function isStaleForToday(codeName, now = /* @__PURE__ */ new Date(), timezone) {
|
|
151
|
-
const file = readFile(codeName);
|
|
152
|
-
if (!file.current) return false;
|
|
153
|
-
return file.current.date !== todayLocalIso(now, timezone);
|
|
154
|
-
}
|
|
155
|
-
function peekCurrentSession(codeName) {
|
|
156
|
-
return readFile(codeName).current;
|
|
157
|
-
}
|
|
158
|
-
var _internals = { todayLocalIso, dailySessionPath, profileDir, encodeProjectPath };
|
|
159
|
-
|
|
160
|
-
export {
|
|
161
|
-
getOrCreateDailySession,
|
|
162
|
-
markDailySessionSpawn,
|
|
163
|
-
rotateDailySession,
|
|
164
|
-
sessionFileExists,
|
|
165
|
-
sessionTranscriptDir,
|
|
166
|
-
sessionFilePath,
|
|
167
|
-
transcriptActivityAgeSeconds,
|
|
168
|
-
isAgentIdle,
|
|
169
|
-
isStaleForToday,
|
|
170
|
-
peekCurrentSession,
|
|
171
|
-
_internals
|
|
172
|
-
};
|
|
173
|
-
//# sourceMappingURL=chunk-354FAVQR.js.map
|