clisbot 0.1.46-beta.0 → 0.1.50
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/README.md +167 -40
- package/config/clisbot.json.template +1 -1
- package/dist/main.js +1317 -463
- package/package.json +1 -1
- package/templates/customized/README.md +5 -4
- package/templates/customized/personal-assistant/CLAUDE.md +0 -215
- package/templates/customized/personal-assistant/GEMINI.md +0 -136
- package/templates/customized/team-assistant/CLAUDE.md +0 -216
- package/templates/customized/team-assistant/GEMINI.md +0 -137
package/dist/main.js
CHANGED
|
@@ -32451,7 +32451,7 @@ var require_websocket = __commonJS((exports, module) => {
|
|
|
32451
32451
|
var http = __require("http");
|
|
32452
32452
|
var net = __require("net");
|
|
32453
32453
|
var tls = __require("tls");
|
|
32454
|
-
var { randomBytes, createHash } = __require("crypto");
|
|
32454
|
+
var { randomBytes, createHash: createHash2 } = __require("crypto");
|
|
32455
32455
|
var { Duplex, Readable } = __require("stream");
|
|
32456
32456
|
var { URL: URL2 } = __require("url");
|
|
32457
32457
|
var PerMessageDeflate = require_permessage_deflate();
|
|
@@ -32990,7 +32990,7 @@ var require_websocket = __commonJS((exports, module) => {
|
|
|
32990
32990
|
abortHandshake(websocket, socket, "Invalid Upgrade header");
|
|
32991
32991
|
return;
|
|
32992
32992
|
}
|
|
32993
|
-
const digest =
|
|
32993
|
+
const digest = createHash2("sha1").update(key + GUID).digest("base64");
|
|
32994
32994
|
if (res.headers["sec-websocket-accept"] !== digest) {
|
|
32995
32995
|
abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
|
|
32996
32996
|
return;
|
|
@@ -33363,7 +33363,7 @@ var require_websocket_server = __commonJS((exports, module) => {
|
|
|
33363
33363
|
var EventEmitter = __require("events");
|
|
33364
33364
|
var http = __require("http");
|
|
33365
33365
|
var { Duplex } = __require("stream");
|
|
33366
|
-
var { createHash } = __require("crypto");
|
|
33366
|
+
var { createHash: createHash2 } = __require("crypto");
|
|
33367
33367
|
var extension = require_extension();
|
|
33368
33368
|
var PerMessageDeflate = require_permessage_deflate();
|
|
33369
33369
|
var subprotocol = require_subprotocol();
|
|
@@ -33576,7 +33576,7 @@ var require_websocket_server = __commonJS((exports, module) => {
|
|
|
33576
33576
|
}
|
|
33577
33577
|
if (this._state > RUNNING)
|
|
33578
33578
|
return abortHandshake(socket, 503);
|
|
33579
|
-
const digest =
|
|
33579
|
+
const digest = createHash2("sha1").update(key + GUID).digest("base64");
|
|
33580
33580
|
const headers = [
|
|
33581
33581
|
"HTTP/1.1 101 Switching Protocols",
|
|
33582
33582
|
"Upgrade: websocket",
|
|
@@ -54755,6 +54755,11 @@ function renderCliHelp() {
|
|
|
54755
54755
|
" personal One human gets one dedicated long-lived assistant workspace and session path.",
|
|
54756
54756
|
" team One shared channel or group routes into one shared assistant workspace and session path.",
|
|
54757
54757
|
"",
|
|
54758
|
+
"Bootstrap files:",
|
|
54759
|
+
" `--bot-type` seeds workspace guidance files for a fresh agent. It is optional when you already have a workspace.",
|
|
54760
|
+
" Canonical workspace instructions live in `AGENTS.md`.",
|
|
54761
|
+
" Claude and Gemini bootstraps also add `CLAUDE.md` or `GEMINI.md` as symlinks to `AGENTS.md` for CLI discovery.",
|
|
54762
|
+
"",
|
|
54758
54763
|
"Credential input rules:",
|
|
54759
54764
|
" Pass ENV_NAME or ${ENV_NAME} to keep the selected bot env-backed.",
|
|
54760
54765
|
" Pass a raw or shell-expanded token value to use credentialType=mem for the current runtime only.",
|
|
@@ -54803,7 +54808,7 @@ function renderCliHelp() {
|
|
|
54803
54808
|
" status Show runtime process, config, log, tmux socket status, and recent runner sessions.",
|
|
54804
54809
|
" version Show the installed clisbot version.",
|
|
54805
54810
|
" logs Print the most recent clisbot log lines.",
|
|
54806
|
-
" update Print the
|
|
54811
|
+
" update Print the update guide and release/migration doc links.",
|
|
54807
54812
|
` See ${renderCliCommand("update --help", { inline: true })} before asking an agent to update clisbot.`,
|
|
54808
54813
|
" timezone Manage the app-wide wall-clock timezone used by schedules and loops.",
|
|
54809
54814
|
` See ${renderCliCommand("timezone --help", { inline: true })} for override guidance.`,
|
|
@@ -58902,10 +58907,12 @@ function parseCommandDurationMs(raw) {
|
|
|
58902
58907
|
// src/agents/loop-command.ts
|
|
58903
58908
|
var DEFAULT_LOOP_MAX_TIMES = 50;
|
|
58904
58909
|
var LOOP_FORCE_FLAG = "--force";
|
|
58910
|
+
var LOOP_START_FLAG = "--loop-start";
|
|
58905
58911
|
var LOOP_ALL_FLAG = "--all";
|
|
58906
58912
|
var LOOP_APP_FLAG = "--app";
|
|
58907
58913
|
var MIN_LOOP_INTERVAL_MS = 60000;
|
|
58908
58914
|
var FORCE_LOOP_INTERVAL_MS = 5 * 60000;
|
|
58915
|
+
var LOOP_START_MODES = ["none", "brief", "full"];
|
|
58909
58916
|
var LOOP_WEEKDAY_LABELS = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"];
|
|
58910
58917
|
var LOOP_WEEKDAY_ALIASES = {
|
|
58911
58918
|
sun: 0,
|
|
@@ -58934,7 +58941,12 @@ function parseLoopSlashCommand(raw) {
|
|
|
58934
58941
|
error: "Loop requires an interval, count, or schedule. Try `/loop 5m check CI`, `/loop 3 check CI`, `/loop every day at 07:00 check CI`, or `/loop 3` for maintenance mode."
|
|
58935
58942
|
};
|
|
58936
58943
|
}
|
|
58937
|
-
const
|
|
58944
|
+
const modifier = extractLoopStartModifier(trimmed);
|
|
58945
|
+
if ("error" in modifier) {
|
|
58946
|
+
return modifier;
|
|
58947
|
+
}
|
|
58948
|
+
const normalizedText = modifier.normalizedText;
|
|
58949
|
+
const tokens = modifier.normalizedText.split(/\s+/).filter(Boolean);
|
|
58938
58950
|
const forceTokenIndexes = tokens.map((token, index) => token.toLowerCase() === LOOP_FORCE_FLAG ? index : -1).filter((index) => index >= 0);
|
|
58939
58951
|
if (forceTokenIndexes.length > 1) {
|
|
58940
58952
|
return {
|
|
@@ -58943,7 +58955,7 @@ function parseLoopSlashCommand(raw) {
|
|
|
58943
58955
|
}
|
|
58944
58956
|
const forceTokenIndex = forceTokenIndexes[0];
|
|
58945
58957
|
const leadingToken = tokens[0] ?? "";
|
|
58946
|
-
const everyDayMatch =
|
|
58958
|
+
const everyDayMatch = normalizedText.match(/^every\s+day\s+at\s+(\S+)(?:\s+(.*))?$/i);
|
|
58947
58959
|
if (everyDayMatch) {
|
|
58948
58960
|
if (forceTokenIndex !== undefined) {
|
|
58949
58961
|
return {
|
|
@@ -58956,7 +58968,7 @@ function parseLoopSlashCommand(raw) {
|
|
|
58956
58968
|
error: "Loop wall-clock time must use `HH:MM` in 24-hour format."
|
|
58957
58969
|
};
|
|
58958
58970
|
}
|
|
58959
|
-
|
|
58971
|
+
const parsed = {
|
|
58960
58972
|
mode: "calendar",
|
|
58961
58973
|
cadence: "daily",
|
|
58962
58974
|
localTime: parsedTime.localTime,
|
|
@@ -58966,8 +58978,18 @@ function parseLoopSlashCommand(raw) {
|
|
|
58966
58978
|
force: false,
|
|
58967
58979
|
syntax: "calendar-at"
|
|
58968
58980
|
};
|
|
58981
|
+
const validationError = validateLoopStartModifierPlacement(parsed, modifier, modifier.tokens.length);
|
|
58982
|
+
if (validationError) {
|
|
58983
|
+
return {
|
|
58984
|
+
error: validationError
|
|
58985
|
+
};
|
|
58986
|
+
}
|
|
58987
|
+
return {
|
|
58988
|
+
...parsed,
|
|
58989
|
+
...modifier.loopStart ? { loopStart: modifier.loopStart } : {}
|
|
58990
|
+
};
|
|
58969
58991
|
}
|
|
58970
|
-
const everyWeekdayMatch =
|
|
58992
|
+
const everyWeekdayMatch = normalizedText.match(/^every\s+weekday\s+at\s+(\S+)(?:\s+(.*))?$/i);
|
|
58971
58993
|
if (everyWeekdayMatch) {
|
|
58972
58994
|
if (forceTokenIndex !== undefined) {
|
|
58973
58995
|
return {
|
|
@@ -58980,7 +59002,7 @@ function parseLoopSlashCommand(raw) {
|
|
|
58980
59002
|
error: "Loop wall-clock time must use `HH:MM` in 24-hour format."
|
|
58981
59003
|
};
|
|
58982
59004
|
}
|
|
58983
|
-
|
|
59005
|
+
const parsed = {
|
|
58984
59006
|
mode: "calendar",
|
|
58985
59007
|
cadence: "weekday",
|
|
58986
59008
|
localTime: parsedTime.localTime,
|
|
@@ -58990,8 +59012,18 @@ function parseLoopSlashCommand(raw) {
|
|
|
58990
59012
|
force: false,
|
|
58991
59013
|
syntax: "calendar-at"
|
|
58992
59014
|
};
|
|
59015
|
+
const validationError = validateLoopStartModifierPlacement(parsed, modifier, modifier.tokens.length);
|
|
59016
|
+
if (validationError) {
|
|
59017
|
+
return {
|
|
59018
|
+
error: validationError
|
|
59019
|
+
};
|
|
59020
|
+
}
|
|
59021
|
+
return {
|
|
59022
|
+
...parsed,
|
|
59023
|
+
...modifier.loopStart ? { loopStart: modifier.loopStart } : {}
|
|
59024
|
+
};
|
|
58993
59025
|
}
|
|
58994
|
-
const everyDayOfWeekMatch =
|
|
59026
|
+
const everyDayOfWeekMatch = normalizedText.match(/^every\s+([a-z]+)\s+at\s+(\S+)(?:\s+(.*))?$/i);
|
|
58995
59027
|
if (everyDayOfWeekMatch) {
|
|
58996
59028
|
const dayOfWeek = resolveLoopDayOfWeek(everyDayOfWeekMatch[1] ?? "");
|
|
58997
59029
|
if (dayOfWeek != null) {
|
|
@@ -59006,7 +59038,7 @@ function parseLoopSlashCommand(raw) {
|
|
|
59006
59038
|
error: "Loop wall-clock time must use `HH:MM` in 24-hour format."
|
|
59007
59039
|
};
|
|
59008
59040
|
}
|
|
59009
|
-
|
|
59041
|
+
const parsed = {
|
|
59010
59042
|
mode: "calendar",
|
|
59011
59043
|
cadence: "day-of-week",
|
|
59012
59044
|
dayOfWeek,
|
|
@@ -59017,6 +59049,16 @@ function parseLoopSlashCommand(raw) {
|
|
|
59017
59049
|
force: false,
|
|
59018
59050
|
syntax: "calendar-at"
|
|
59019
59051
|
};
|
|
59052
|
+
const validationError = validateLoopStartModifierPlacement(parsed, modifier, modifier.tokens.length);
|
|
59053
|
+
if (validationError) {
|
|
59054
|
+
return {
|
|
59055
|
+
error: validationError
|
|
59056
|
+
};
|
|
59057
|
+
}
|
|
59058
|
+
return {
|
|
59059
|
+
...parsed,
|
|
59060
|
+
...modifier.loopStart ? { loopStart: modifier.loopStart } : {}
|
|
59061
|
+
};
|
|
59020
59062
|
}
|
|
59021
59063
|
}
|
|
59022
59064
|
const leadingIntervalMs = parseCommandDurationMs(leadingToken);
|
|
@@ -59028,15 +59070,30 @@ function parseLoopSlashCommand(raw) {
|
|
|
59028
59070
|
}
|
|
59029
59071
|
const promptTokens = tokens.slice(forceTokenIndex === 1 ? 2 : 1);
|
|
59030
59072
|
const promptText = promptTokens.join(" ").trim() || undefined;
|
|
59031
|
-
|
|
59073
|
+
const parsed = {
|
|
59032
59074
|
mode: "interval",
|
|
59033
59075
|
intervalMs: leadingIntervalMs,
|
|
59034
59076
|
promptText,
|
|
59035
59077
|
force: forceTokenIndex === 1,
|
|
59036
59078
|
syntax: "leading-interval"
|
|
59037
59079
|
};
|
|
59080
|
+
const validationError = validateLoopStartModifierPlacement(parsed, modifier, modifier.tokens.length);
|
|
59081
|
+
if (validationError) {
|
|
59082
|
+
return {
|
|
59083
|
+
error: validationError
|
|
59084
|
+
};
|
|
59085
|
+
}
|
|
59086
|
+
return {
|
|
59087
|
+
...parsed,
|
|
59088
|
+
...modifier.loopStart ? { loopStart: modifier.loopStart } : {}
|
|
59089
|
+
};
|
|
59038
59090
|
}
|
|
59039
59091
|
if (/^-?\d+$/.test(leadingToken)) {
|
|
59092
|
+
if (modifier.loopStart) {
|
|
59093
|
+
return {
|
|
59094
|
+
error: `\`${LOOP_START_FLAG}\` is only supported for recurring interval and wall-clock loops.`
|
|
59095
|
+
};
|
|
59096
|
+
}
|
|
59040
59097
|
if (forceTokenIndex !== undefined) {
|
|
59041
59098
|
return {
|
|
59042
59099
|
error: `\`${LOOP_FORCE_FLAG}\` is only supported for interval loops.`
|
|
@@ -59057,8 +59114,13 @@ function parseLoopSlashCommand(raw) {
|
|
|
59057
59114
|
syntax: "leading-count"
|
|
59058
59115
|
};
|
|
59059
59116
|
}
|
|
59060
|
-
const trailingTimes =
|
|
59117
|
+
const trailingTimes = normalizedText.match(/^(.*?)(?:\s+)?(-?\d+)\s+times$/i);
|
|
59061
59118
|
if (trailingTimes) {
|
|
59119
|
+
if (modifier.loopStart) {
|
|
59120
|
+
return {
|
|
59121
|
+
error: `\`${LOOP_START_FLAG}\` is only supported for recurring interval and wall-clock loops.`
|
|
59122
|
+
};
|
|
59123
|
+
}
|
|
59062
59124
|
if (forceTokenIndex !== undefined) {
|
|
59063
59125
|
return {
|
|
59064
59126
|
error: `\`${LOOP_FORCE_FLAG}\` is only supported for interval loops.`
|
|
@@ -59079,7 +59141,7 @@ function parseLoopSlashCommand(raw) {
|
|
|
59079
59141
|
syntax: "trailing-times"
|
|
59080
59142
|
};
|
|
59081
59143
|
}
|
|
59082
|
-
const withoutTrailingForce = forceTokenIndex === tokens.length - 1 ? tokens.slice(0, -1).join(" ") :
|
|
59144
|
+
const withoutTrailingForce = forceTokenIndex === tokens.length - 1 ? tokens.slice(0, -1).join(" ") : normalizedText;
|
|
59083
59145
|
const normalizedEveryInput = withoutTrailingForce.trim();
|
|
59084
59146
|
const hasTrailingForce = forceTokenIndex === tokens.length - 1;
|
|
59085
59147
|
if (forceTokenIndex !== undefined && !hasTrailingForce) {
|
|
@@ -59096,13 +59158,23 @@ function parseLoopSlashCommand(raw) {
|
|
|
59096
59158
|
};
|
|
59097
59159
|
}
|
|
59098
59160
|
const promptText = everyCompactClause[1]?.trim() || undefined;
|
|
59099
|
-
|
|
59161
|
+
const parsed = {
|
|
59100
59162
|
mode: "interval",
|
|
59101
59163
|
intervalMs,
|
|
59102
59164
|
promptText,
|
|
59103
59165
|
force: hasTrailingForce,
|
|
59104
59166
|
syntax: "every-clause"
|
|
59105
59167
|
};
|
|
59168
|
+
const validationError = validateLoopStartModifierPlacement(parsed, modifier, modifier.tokens.length);
|
|
59169
|
+
if (validationError) {
|
|
59170
|
+
return {
|
|
59171
|
+
error: validationError
|
|
59172
|
+
};
|
|
59173
|
+
}
|
|
59174
|
+
return {
|
|
59175
|
+
...parsed,
|
|
59176
|
+
...modifier.loopStart ? { loopStart: modifier.loopStart } : {}
|
|
59177
|
+
};
|
|
59106
59178
|
}
|
|
59107
59179
|
const everyClause = normalizedEveryInput.match(/^(.*?)(?:\s+)?every\s+(-?\d+)\s+([a-z]+)$/i);
|
|
59108
59180
|
if (everyClause) {
|
|
@@ -59120,18 +59192,84 @@ function parseLoopSlashCommand(raw) {
|
|
|
59120
59192
|
};
|
|
59121
59193
|
}
|
|
59122
59194
|
const promptText = everyClause[1]?.trim() || undefined;
|
|
59123
|
-
|
|
59195
|
+
const parsed = {
|
|
59124
59196
|
mode: "interval",
|
|
59125
59197
|
intervalMs,
|
|
59126
59198
|
promptText,
|
|
59127
59199
|
force: hasTrailingForce,
|
|
59128
59200
|
syntax: "every-clause"
|
|
59129
59201
|
};
|
|
59202
|
+
const validationError = validateLoopStartModifierPlacement(parsed, modifier, modifier.tokens.length);
|
|
59203
|
+
if (validationError) {
|
|
59204
|
+
return {
|
|
59205
|
+
error: validationError
|
|
59206
|
+
};
|
|
59207
|
+
}
|
|
59208
|
+
return {
|
|
59209
|
+
...parsed,
|
|
59210
|
+
...modifier.loopStart ? { loopStart: modifier.loopStart } : {}
|
|
59211
|
+
};
|
|
59130
59212
|
}
|
|
59131
59213
|
return {
|
|
59132
59214
|
error: "Loop requires an interval, count, or schedule. Try `/loop 5m check CI`, `/loop 3 check CI`, `/loop every day at 07:00 check CI`, or `/loop 3` for maintenance mode."
|
|
59133
59215
|
};
|
|
59134
59216
|
}
|
|
59217
|
+
function extractLoopStartModifier(raw) {
|
|
59218
|
+
const tokens = raw.split(/\s+/).filter(Boolean);
|
|
59219
|
+
const indexes = tokens.map((token, index) => token.trim().toLowerCase() === LOOP_START_FLAG ? index : -1).filter((index) => index >= 0);
|
|
59220
|
+
if (indexes.length > 1) {
|
|
59221
|
+
return {
|
|
59222
|
+
error: `Loop accepts at most one \`${LOOP_START_FLAG}\` flag.`
|
|
59223
|
+
};
|
|
59224
|
+
}
|
|
59225
|
+
const flagIndex = indexes[0];
|
|
59226
|
+
if (flagIndex == null) {
|
|
59227
|
+
return {
|
|
59228
|
+
normalizedText: raw,
|
|
59229
|
+
tokens
|
|
59230
|
+
};
|
|
59231
|
+
}
|
|
59232
|
+
const rawMode = tokens[flagIndex + 1]?.trim().toLowerCase();
|
|
59233
|
+
if (!rawMode) {
|
|
59234
|
+
return {
|
|
59235
|
+
error: `Loop requires \`${LOOP_START_FLAG} <none|brief|full>\`.`
|
|
59236
|
+
};
|
|
59237
|
+
}
|
|
59238
|
+
if (!LOOP_START_MODES.includes(rawMode)) {
|
|
59239
|
+
return {
|
|
59240
|
+
error: `\`${LOOP_START_FLAG}\` must be one of \`none\`, \`brief\`, or \`full\`.`
|
|
59241
|
+
};
|
|
59242
|
+
}
|
|
59243
|
+
const strippedTokens = tokens.filter((_, index) => index !== flagIndex && index !== flagIndex + 1);
|
|
59244
|
+
return {
|
|
59245
|
+
normalizedText: strippedTokens.join(" "),
|
|
59246
|
+
tokens,
|
|
59247
|
+
flagIndex,
|
|
59248
|
+
loopStart: rawMode
|
|
59249
|
+
};
|
|
59250
|
+
}
|
|
59251
|
+
function validateLoopStartModifierPlacement(parsed, modifier, tokenCount) {
|
|
59252
|
+
if (!modifier.loopStart || modifier.flagIndex == null) {
|
|
59253
|
+
return;
|
|
59254
|
+
}
|
|
59255
|
+
if (parsed.mode === "calendar") {
|
|
59256
|
+
if (modifier.flagIndex === 4) {
|
|
59257
|
+
return;
|
|
59258
|
+
}
|
|
59259
|
+
return `For wall-clock loops, \`${LOOP_START_FLAG}\` must appear immediately after the \`at HH:MM\` clause, for example \`/loop every day at 07:00 --loop-start none morning brief\`.`;
|
|
59260
|
+
}
|
|
59261
|
+
if (parsed.syntax === "leading-interval") {
|
|
59262
|
+
const expectedIndex = parsed.force ? 2 : 1;
|
|
59263
|
+
if (modifier.flagIndex === expectedIndex) {
|
|
59264
|
+
return;
|
|
59265
|
+
}
|
|
59266
|
+
return `For leading interval loops, \`${LOOP_START_FLAG}\` must appear after the interval and optional \`${LOOP_FORCE_FLAG}\`, before the prompt, for example \`/loop 5m --loop-start none check CI\`.`;
|
|
59267
|
+
}
|
|
59268
|
+
if (modifier.flagIndex === tokenCount - 2) {
|
|
59269
|
+
return;
|
|
59270
|
+
}
|
|
59271
|
+
return `For \`every ...\` interval loops, \`${LOOP_START_FLAG}\` must appear at the end of the loop schedule, for example \`/loop check deploy every 2h --loop-start none\`.`;
|
|
59272
|
+
}
|
|
59135
59273
|
function formatLoopIntervalShort(intervalMs) {
|
|
59136
59274
|
if (intervalMs % (60 * 60000) === 0) {
|
|
59137
59275
|
return `${intervalMs / (60 * 60000)}h`;
|
|
@@ -59161,7 +59299,8 @@ function renderLoopHelpLines() {
|
|
|
59161
59299
|
"- `/loop /codereview 3 times`: run the slash command 3 times",
|
|
59162
59300
|
"- `/loop status`: show active loops for this session",
|
|
59163
59301
|
`- \`/loop cancel\`, \`/loop cancel <id>\`, \`/loop cancel --all\`, \`/loop cancel --all ${LOOP_APP_FLAG}\`: cancel active loops`,
|
|
59164
|
-
`- intervals must be at least \`1m\`; intervals below \`5m\` require \`${LOOP_FORCE_FLAG}\` right after the interval clause; wall-clock schedules use \`every ... at HH:MM\`; the first wall-clock loop created with \`clisbot loops create\` requires \`--confirm\`; timezone resolves from route/topic, agent, bot, app timezone, then legacy defaults and host; bare numbers mean times, compact durations such as \`5m\` mean intervals, and the historical default loop cap was \`${DEFAULT_LOOP_MAX_TIMES}
|
|
59302
|
+
`- intervals must be at least \`1m\`; intervals below \`5m\` require \`${LOOP_FORCE_FLAG}\` right after the interval clause; wall-clock schedules use \`every ... at HH:MM\`; the first wall-clock loop created with \`clisbot loops create\` requires \`--confirm\`; timezone resolves from route/topic, agent, bot, app timezone, then legacy defaults and host; bare numbers mean times, compact durations such as \`5m\` mean intervals, and the historical default loop cap was \`${DEFAULT_LOOP_MAX_TIMES}\``,
|
|
59303
|
+
`- Advanced: loop creation also accepts \`${LOOP_START_FLAG} <none|brief|full>\` to override the default start notification behavior for that loop. Example: \`/loop every day at 07:00 --loop-start none morning brief\``
|
|
59165
59304
|
];
|
|
59166
59305
|
}
|
|
59167
59306
|
function hasLoopFlag(raw, flag) {
|
|
@@ -60287,8 +60426,8 @@ function migrateLegacyConfigShape(config) {
|
|
|
60287
60426
|
}
|
|
60288
60427
|
|
|
60289
60428
|
// src/config/config-migration.ts
|
|
60290
|
-
var CURRENT_SCHEMA_VERSION = "0.1.
|
|
60291
|
-
var
|
|
60429
|
+
var CURRENT_SCHEMA_VERSION = "0.1.50";
|
|
60430
|
+
var LEGACY_CONFIG_UPGRADE_MAX_SCHEMA_VERSION = "0.1.44";
|
|
60292
60431
|
function isRecord3(value) {
|
|
60293
60432
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
60294
60433
|
}
|
|
@@ -60322,6 +60461,22 @@ function isAtMostVersion(schemaVersion, maxVersion) {
|
|
|
60322
60461
|
}
|
|
60323
60462
|
return true;
|
|
60324
60463
|
}
|
|
60464
|
+
function isBeforeVersion(schemaVersion, targetVersion) {
|
|
60465
|
+
const current = parseVersionParts(schemaVersion);
|
|
60466
|
+
const target = parseVersionParts(targetVersion);
|
|
60467
|
+
if (!current || !target) {
|
|
60468
|
+
return !schemaVersion;
|
|
60469
|
+
}
|
|
60470
|
+
for (let index = 0;index < current.length; index += 1) {
|
|
60471
|
+
if (current[index] < target[index]) {
|
|
60472
|
+
return true;
|
|
60473
|
+
}
|
|
60474
|
+
if (current[index] > target[index]) {
|
|
60475
|
+
return false;
|
|
60476
|
+
}
|
|
60477
|
+
}
|
|
60478
|
+
return false;
|
|
60479
|
+
}
|
|
60325
60480
|
function readTimezone(value) {
|
|
60326
60481
|
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
60327
60482
|
}
|
|
@@ -60572,7 +60727,10 @@ function updateSchemaVersion(config) {
|
|
|
60572
60727
|
};
|
|
60573
60728
|
}
|
|
60574
60729
|
function shouldUpgradeConfigSchema(schemaVersion) {
|
|
60575
|
-
return
|
|
60730
|
+
return isBeforeVersion(schemaVersion, CURRENT_SCHEMA_VERSION);
|
|
60731
|
+
}
|
|
60732
|
+
function shouldApplyLegacyConfigMigration(schemaVersion) {
|
|
60733
|
+
return isAtMostVersion(schemaVersion, LEGACY_CONFIG_UPGRADE_MAX_SCHEMA_VERSION);
|
|
60576
60734
|
}
|
|
60577
60735
|
function normalizeConfigDocumentShape(input) {
|
|
60578
60736
|
if (!isRecord3(input)) {
|
|
@@ -60581,7 +60739,7 @@ function normalizeConfigDocumentShape(input) {
|
|
|
60581
60739
|
const config = { ...input };
|
|
60582
60740
|
migrateLegacyConfigShape(config);
|
|
60583
60741
|
const schemaVersion = isRecord3(config.meta) && typeof config.meta.schemaVersion === "string" ? config.meta.schemaVersion : undefined;
|
|
60584
|
-
const legacyExactAdmission =
|
|
60742
|
+
const legacyExactAdmission = shouldApplyLegacyConfigMigration(schemaVersion);
|
|
60585
60743
|
const bots = cloneRecord2(config.bots);
|
|
60586
60744
|
const slack = cloneRecord2(bots.slack);
|
|
60587
60745
|
const telegram = cloneRecord2(bots.telegram);
|
|
@@ -62066,7 +62224,7 @@ import { basename, dirname as dirname4, join as join5 } from "node:path";
|
|
|
62066
62224
|
|
|
62067
62225
|
// src/config/persisted-config.ts
|
|
62068
62226
|
var defaultOwnedRunnerFields = {
|
|
62069
|
-
codex: ["startupReadyPattern"],
|
|
62227
|
+
codex: ["startupDelayMs", "startupReadyPattern"],
|
|
62070
62228
|
gemini: [
|
|
62071
62229
|
"startupDelayMs",
|
|
62072
62230
|
"startupRetryCount",
|
|
@@ -62228,8 +62386,17 @@ function pruneCurrentSchemaStartupDefaults(config) {
|
|
|
62228
62386
|
const defaults = isRecord6(agents?.defaults) ? agents.defaults : undefined;
|
|
62229
62387
|
const runner = isRecord6(defaults?.runner) ? defaults.runner : undefined;
|
|
62230
62388
|
const runnerDefaults = isRecord6(runner?.defaults) ? runner.defaults : undefined;
|
|
62389
|
+
const codexRunner = isRecord6(runner?.codex) ? runner.codex : undefined;
|
|
62390
|
+
let pruned = false;
|
|
62231
62391
|
if (runnerDefaults?.startupDelayMs === 3000) {
|
|
62232
62392
|
delete runnerDefaults.startupDelayMs;
|
|
62393
|
+
pruned = true;
|
|
62394
|
+
}
|
|
62395
|
+
if (codexRunner?.startupDelayMs === 3000) {
|
|
62396
|
+
delete codexRunner.startupDelayMs;
|
|
62397
|
+
pruned = true;
|
|
62398
|
+
}
|
|
62399
|
+
if (pruned) {
|
|
62233
62400
|
nextConfig.meta = {
|
|
62234
62401
|
...meta,
|
|
62235
62402
|
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
@@ -63254,7 +63421,7 @@ async function runPairingCli(args, writer = console) {
|
|
|
63254
63421
|
|
|
63255
63422
|
// src/agents/bootstrap.ts
|
|
63256
63423
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
63257
|
-
import { cpSync, existsSync as existsSync4, readdirSync, statSync as statSync2 } from "node:fs";
|
|
63424
|
+
import { cpSync, existsSync as existsSync4, lstatSync, readdirSync, rmSync as rmSync2, statSync as statSync2, symlinkSync } from "node:fs";
|
|
63258
63425
|
import { dirname as dirname6, join as join6 } from "node:path";
|
|
63259
63426
|
function resolveTemplateRoot(moduleDir) {
|
|
63260
63427
|
const candidates = [
|
|
@@ -63272,35 +63439,40 @@ var TEMPLATE_ROOT = resolveTemplateRoot(dirname6(fileURLToPath2(import.meta.url)
|
|
|
63272
63439
|
var DEFAULT_TEMPLATE_DIR = join6(TEMPLATE_ROOT, "default");
|
|
63273
63440
|
var CUSTOMIZED_TEMPLATE_DIR = join6(TEMPLATE_ROOT, "customized");
|
|
63274
63441
|
var CUSTOMIZED_DEFAULT_TEMPLATE_DIR = join6(CUSTOMIZED_TEMPLATE_DIR, "default");
|
|
63275
|
-
var
|
|
63276
|
-
|
|
63442
|
+
var CANONICAL_BOOTSTRAP_FILE = "AGENTS.md";
|
|
63443
|
+
var TOOL_DISCOVERY_FILE = {
|
|
63277
63444
|
claude: "CLAUDE.md",
|
|
63278
63445
|
gemini: "GEMINI.md"
|
|
63279
63446
|
};
|
|
63280
|
-
function
|
|
63281
|
-
|
|
63282
|
-
|
|
63283
|
-
return
|
|
63447
|
+
function pathExists(path2) {
|
|
63448
|
+
try {
|
|
63449
|
+
lstatSync(path2);
|
|
63450
|
+
return true;
|
|
63451
|
+
} catch {
|
|
63452
|
+
return false;
|
|
63284
63453
|
}
|
|
63454
|
+
}
|
|
63455
|
+
function shouldIncludeTemplateFile(relativePath) {
|
|
63456
|
+
const normalized = relativePath.replaceAll("\\", "/");
|
|
63285
63457
|
if (normalized.endsWith("CLAUDE.md")) {
|
|
63286
|
-
return
|
|
63458
|
+
return false;
|
|
63287
63459
|
}
|
|
63288
63460
|
if (normalized.endsWith("GEMINI.md")) {
|
|
63289
|
-
return
|
|
63461
|
+
return false;
|
|
63290
63462
|
}
|
|
63291
63463
|
return true;
|
|
63292
63464
|
}
|
|
63293
|
-
function collectTemplateFiles(rootDir,
|
|
63465
|
+
function collectTemplateFiles(rootDir, prefix = "") {
|
|
63294
63466
|
const files = [];
|
|
63295
63467
|
for (const entry of readdirSync(rootDir)) {
|
|
63296
63468
|
const sourcePath = join6(rootDir, entry);
|
|
63297
63469
|
const relativePath = prefix ? join6(prefix, entry) : entry;
|
|
63298
63470
|
const sourceStat = statSync2(sourcePath);
|
|
63299
63471
|
if (sourceStat.isDirectory()) {
|
|
63300
|
-
files.push(...collectTemplateFiles(sourcePath,
|
|
63472
|
+
files.push(...collectTemplateFiles(sourcePath, relativePath));
|
|
63301
63473
|
continue;
|
|
63302
63474
|
}
|
|
63303
|
-
if (!shouldIncludeTemplateFile(
|
|
63475
|
+
if (!shouldIncludeTemplateFile(relativePath)) {
|
|
63304
63476
|
continue;
|
|
63305
63477
|
}
|
|
63306
63478
|
files.push({
|
|
@@ -63313,22 +63485,44 @@ function collectTemplateFiles(rootDir, toolId, prefix = "") {
|
|
|
63313
63485
|
}
|
|
63314
63486
|
function getTemplateFiles(toolId, mode) {
|
|
63315
63487
|
return [
|
|
63316
|
-
...collectTemplateFiles(DEFAULT_TEMPLATE_DIR
|
|
63317
|
-
...collectTemplateFiles(CUSTOMIZED_DEFAULT_TEMPLATE_DIR
|
|
63488
|
+
...collectTemplateFiles(DEFAULT_TEMPLATE_DIR),
|
|
63489
|
+
...collectTemplateFiles(CUSTOMIZED_DEFAULT_TEMPLATE_DIR).map((file) => ({
|
|
63318
63490
|
...file,
|
|
63319
63491
|
customized: true
|
|
63320
63492
|
})),
|
|
63321
|
-
...collectTemplateFiles(join6(CUSTOMIZED_TEMPLATE_DIR, mode)
|
|
63493
|
+
...collectTemplateFiles(join6(CUSTOMIZED_TEMPLATE_DIR, mode)).map((file) => ({
|
|
63322
63494
|
...file,
|
|
63323
63495
|
customized: true
|
|
63324
63496
|
}))
|
|
63325
63497
|
];
|
|
63326
63498
|
}
|
|
63499
|
+
function getBootstrapManagedPaths(toolId, mode) {
|
|
63500
|
+
const paths = new Set(getTemplateFiles(toolId, mode).map((file) => file.relativePath));
|
|
63501
|
+
const discoveryFile = TOOL_DISCOVERY_FILE[toolId];
|
|
63502
|
+
if (discoveryFile) {
|
|
63503
|
+
paths.add(discoveryFile);
|
|
63504
|
+
}
|
|
63505
|
+
return [...paths];
|
|
63506
|
+
}
|
|
63327
63507
|
function getBootstrapTemplateConflicts(workspacePath, toolId, mode) {
|
|
63328
63508
|
if (!existsSync4(workspacePath)) {
|
|
63329
63509
|
return [];
|
|
63330
63510
|
}
|
|
63331
|
-
return
|
|
63511
|
+
return getBootstrapManagedPaths(toolId, mode).filter((relativePath) => pathExists(join6(workspacePath, relativePath)));
|
|
63512
|
+
}
|
|
63513
|
+
function writeToolDiscoverySymlink(workspacePath, toolId, options) {
|
|
63514
|
+
const discoveryFile = TOOL_DISCOVERY_FILE[toolId];
|
|
63515
|
+
if (!discoveryFile) {
|
|
63516
|
+
return;
|
|
63517
|
+
}
|
|
63518
|
+
const destinationPath = join6(workspacePath, discoveryFile);
|
|
63519
|
+
if (options?.force) {
|
|
63520
|
+
rmSync2(destinationPath, {
|
|
63521
|
+
force: true,
|
|
63522
|
+
recursive: false
|
|
63523
|
+
});
|
|
63524
|
+
}
|
|
63525
|
+
symlinkSync(CANONICAL_BOOTSTRAP_FILE, destinationPath);
|
|
63332
63526
|
}
|
|
63333
63527
|
async function applyBootstrapTemplate(workspacePath, mode, toolId, options) {
|
|
63334
63528
|
const force = options?.force === true;
|
|
@@ -63344,6 +63538,9 @@ async function applyBootstrapTemplate(workspacePath, mode, toolId, options) {
|
|
|
63344
63538
|
force: force || file.customized
|
|
63345
63539
|
});
|
|
63346
63540
|
}
|
|
63541
|
+
writeToolDiscoverySymlink(workspacePath, toolId, {
|
|
63542
|
+
force
|
|
63543
|
+
});
|
|
63347
63544
|
}
|
|
63348
63545
|
function getBootstrapWorkspaceState(workspacePath, mode, toolId) {
|
|
63349
63546
|
if (!mode) {
|
|
@@ -63352,7 +63549,11 @@ function getBootstrapWorkspaceState(workspacePath, mode, toolId) {
|
|
|
63352
63549
|
if (!toolId || !existsSync4(workspacePath)) {
|
|
63353
63550
|
return "missing";
|
|
63354
63551
|
}
|
|
63355
|
-
if (!existsSync4(join6(workspacePath,
|
|
63552
|
+
if (!existsSync4(join6(workspacePath, CANONICAL_BOOTSTRAP_FILE)) || !existsSync4(join6(workspacePath, "IDENTITY.md"))) {
|
|
63553
|
+
return "missing";
|
|
63554
|
+
}
|
|
63555
|
+
const discoveryFile = TOOL_DISCOVERY_FILE[toolId];
|
|
63556
|
+
if (discoveryFile && !existsSync4(join6(workspacePath, discoveryFile))) {
|
|
63356
63557
|
return "missing";
|
|
63357
63558
|
}
|
|
63358
63559
|
if (existsSync4(join6(workspacePath, "BOOTSTRAP.md"))) {
|
|
@@ -63559,6 +63760,9 @@ function renderAgentsHelp() {
|
|
|
63559
63760
|
"Notes:",
|
|
63560
63761
|
` - \`agents add\` is the lower-level manual surface; first-run ${renderCliCommand("start", { inline: true })} and ${renderCliCommand("init", { inline: true })} can bootstrap the first \`default\` agent for you`,
|
|
63561
63762
|
" - `--cli` is required on `agents add`; supported tools are `codex`, `claude`, and `gemini`",
|
|
63763
|
+
" - `agents add` without `--bot-type` is valid and does not seed any bootstrap files",
|
|
63764
|
+
" - `--bot-type` on `agents add` or `agents bootstrap` seeds a fresh workspace template; use it when you want clisbot to create guidance files for you",
|
|
63765
|
+
" - canonical workspace instructions live in `AGENTS.md`; Claude and Gemini add `CLAUDE.md` or `GEMINI.md` as symlinks to that same file",
|
|
63562
63766
|
" - omit `--startup-option` to inherit the built-in startup args for the selected CLI tool",
|
|
63563
63767
|
" - `response-mode` and `additional-message-mode` mutate per-agent overrides under `agents.list[]`",
|
|
63564
63768
|
" - use agent timezone only when one workspace/assistant should run wall-clock loops in a different timezone than app default"
|
|
@@ -64527,7 +64731,7 @@ class RuntimeHealthStore {
|
|
|
64527
64731
|
|
|
64528
64732
|
// src/control/runtime-process.ts
|
|
64529
64733
|
import { execFileSync, spawn as spawn3 } from "node:child_process";
|
|
64530
|
-
import { closeSync, existsSync as existsSync7, openSync, readFileSync as readFileSync3, rmSync as
|
|
64734
|
+
import { closeSync, existsSync as existsSync7, openSync, readFileSync as readFileSync3, rmSync as rmSync4, statSync as statSync4 } from "node:fs";
|
|
64531
64735
|
import { dirname as dirname15, join as join13 } from "node:path";
|
|
64532
64736
|
import { kill as kill2 } from "node:process";
|
|
64533
64737
|
|
|
@@ -65133,14 +65337,20 @@ class TmuxClient {
|
|
|
65133
65337
|
}
|
|
65134
65338
|
return result.stdout;
|
|
65135
65339
|
}
|
|
65340
|
+
sessionTarget(sessionName) {
|
|
65341
|
+
return `=${sessionName}`;
|
|
65342
|
+
}
|
|
65136
65343
|
target(sessionName) {
|
|
65137
|
-
return `${sessionName}:${MAIN_WINDOW_NAME}`;
|
|
65344
|
+
return `${this.sessionTarget(sessionName)}:${MAIN_WINDOW_NAME}`;
|
|
65345
|
+
}
|
|
65346
|
+
windowTarget(sessionName, windowName) {
|
|
65347
|
+
return `${this.sessionTarget(sessionName)}:${windowName}`;
|
|
65138
65348
|
}
|
|
65139
65349
|
rawTarget(target) {
|
|
65140
65350
|
return target;
|
|
65141
65351
|
}
|
|
65142
65352
|
async hasSession(sessionName) {
|
|
65143
|
-
const result = await this.exec(["has-session", "-t", sessionName]);
|
|
65353
|
+
const result = await this.exec(["has-session", "-t", this.sessionTarget(sessionName)]);
|
|
65144
65354
|
return result.exitCode === 0;
|
|
65145
65355
|
}
|
|
65146
65356
|
async listSessions() {
|
|
@@ -65236,14 +65446,14 @@ ${result.stdout}`.trim();
|
|
|
65236
65446
|
"-F",
|
|
65237
65447
|
"#{pane_id}",
|
|
65238
65448
|
"-t",
|
|
65239
|
-
params.sessionName,
|
|
65449
|
+
this.sessionTarget(params.sessionName),
|
|
65240
65450
|
"-n",
|
|
65241
65451
|
params.name,
|
|
65242
65452
|
"-c",
|
|
65243
65453
|
params.cwd,
|
|
65244
65454
|
params.command
|
|
65245
65455
|
]);
|
|
65246
|
-
await this.freezeWindowName(
|
|
65456
|
+
await this.freezeWindowName(this.windowTarget(params.sessionName, params.name));
|
|
65247
65457
|
return paneId.trim();
|
|
65248
65458
|
}
|
|
65249
65459
|
async freezeWindowName(target) {
|
|
@@ -65254,7 +65464,7 @@ ${result.stdout}`.trim();
|
|
|
65254
65464
|
const output = await this.execOrThrow([
|
|
65255
65465
|
"list-windows",
|
|
65256
65466
|
"-t",
|
|
65257
|
-
sessionName,
|
|
65467
|
+
this.sessionTarget(sessionName),
|
|
65258
65468
|
"-F",
|
|
65259
65469
|
"#{window_name}\t#{pane_id}"
|
|
65260
65470
|
]);
|
|
@@ -65330,7 +65540,7 @@ ${result.stdout}`.trim();
|
|
|
65330
65540
|
};
|
|
65331
65541
|
}
|
|
65332
65542
|
async killSession(sessionName) {
|
|
65333
|
-
await this.exec(["kill-session", "-t", sessionName]);
|
|
65543
|
+
await this.exec(["kill-session", "-t", this.sessionTarget(sessionName)]);
|
|
65334
65544
|
}
|
|
65335
65545
|
async killPane(target) {
|
|
65336
65546
|
await this.exec(["kill-pane", "-t", this.rawTarget(target)]);
|
|
@@ -65360,7 +65570,7 @@ ${result.stdout}`.trim();
|
|
|
65360
65570
|
|
|
65361
65571
|
// src/control/runtime-monitor.ts
|
|
65362
65572
|
import { spawn as spawn2 } from "node:child_process";
|
|
65363
|
-
import { existsSync as existsSync6, rmSync as
|
|
65573
|
+
import { existsSync as existsSync6, rmSync as rmSync3 } from "node:fs";
|
|
65364
65574
|
import { dirname as dirname14 } from "node:path";
|
|
65365
65575
|
import { kill } from "node:process";
|
|
65366
65576
|
import { once } from "node:events";
|
|
@@ -65882,12 +66092,19 @@ function parseAgentCommand(text, options = {}) {
|
|
|
65882
66092
|
if (lowered === "loop") {
|
|
65883
66093
|
const loopText = withoutSlash.slice(command.length).trim();
|
|
65884
66094
|
const loweredLoopText = loopText.toLowerCase();
|
|
66095
|
+
const hasLoopStartOverride = hasLoopFlag(loopText, LOOP_START_FLAG);
|
|
65885
66096
|
if (!loweredLoopText || loweredLoopText === "help") {
|
|
65886
66097
|
return {
|
|
65887
66098
|
type: "control",
|
|
65888
66099
|
name: "loop-help"
|
|
65889
66100
|
};
|
|
65890
66101
|
}
|
|
66102
|
+
if (hasLoopStartOverride && (loweredLoopText.startsWith("help ") || loweredLoopText === "status" || loweredLoopText.startsWith("status "))) {
|
|
66103
|
+
return {
|
|
66104
|
+
type: "loop-error",
|
|
66105
|
+
message: `\`${LOOP_START_FLAG}\` is only supported when creating recurring interval and wall-clock loops.`
|
|
66106
|
+
};
|
|
66107
|
+
}
|
|
65891
66108
|
if (loweredLoopText === "status") {
|
|
65892
66109
|
return {
|
|
65893
66110
|
type: "loop-control",
|
|
@@ -65896,6 +66113,12 @@ function parseAgentCommand(text, options = {}) {
|
|
|
65896
66113
|
}
|
|
65897
66114
|
if (loweredLoopText === "cancel" || loweredLoopText.startsWith("cancel ")) {
|
|
65898
66115
|
const cancelArgs = loopText.slice("cancel".length).trim();
|
|
66116
|
+
if (hasLoopStartOverride) {
|
|
66117
|
+
return {
|
|
66118
|
+
type: "loop-error",
|
|
66119
|
+
message: `\`${LOOP_START_FLAG}\` is only supported when creating recurring interval and wall-clock loops.`
|
|
66120
|
+
};
|
|
66121
|
+
}
|
|
65899
66122
|
if (hasLoopFlag(cancelArgs, LOOP_FORCE_FLAG)) {
|
|
65900
66123
|
return {
|
|
65901
66124
|
type: "loop-error",
|
|
@@ -66004,14 +66227,14 @@ function renderAgentControlSlashHelp() {
|
|
|
66004
66227
|
"- `/start`: show onboarding help for the current surface",
|
|
66005
66228
|
"- `/status`: show the current route status and operator setup commands",
|
|
66006
66229
|
"- `/help`: show available control slash commands",
|
|
66007
|
-
"- `/whoami`: show the current platform, route,
|
|
66230
|
+
"- `/whoami`: show the current platform, route, sender identity, and saved session id",
|
|
66008
66231
|
"- `/transcript`: show a short recent session snapshot when the route verbose policy allows it",
|
|
66009
66232
|
"- `/transcript full`: show a longer session snapshot when you need the full pane context",
|
|
66010
66233
|
"- `/attach`: attach this thread to the active run and resume live updates when it is still processing",
|
|
66011
66234
|
"- `/detach`: stop live updates for this thread while still posting the final result here",
|
|
66012
66235
|
"- `/watch every 30s [for 10m]`: post the latest state on an interval until the run settles or the watch window ends",
|
|
66013
66236
|
"- `/stop`: send Escape to interrupt the current conversation session",
|
|
66014
|
-
"- `/new`:
|
|
66237
|
+
"- `/new`: start a new session for this routed conversation and save the new session id",
|
|
66015
66238
|
"- `/nudge`: send one extra Enter to the current tmux session without resending the prompt text",
|
|
66016
66239
|
"- `/followup status`: show the current conversation follow-up policy",
|
|
66017
66240
|
"- `/followup auto`: allow natural follow-up after the bot has replied in-thread",
|
|
@@ -66171,27 +66394,6 @@ class SessionStore {
|
|
|
66171
66394
|
return next;
|
|
66172
66395
|
});
|
|
66173
66396
|
}
|
|
66174
|
-
async touch(params) {
|
|
66175
|
-
const existing = await this.get(params.sessionKey);
|
|
66176
|
-
const sessionId = params.sessionId?.trim() || existing?.sessionId;
|
|
66177
|
-
if (!sessionId) {
|
|
66178
|
-
return null;
|
|
66179
|
-
}
|
|
66180
|
-
return this.put({
|
|
66181
|
-
agentId: params.agentId,
|
|
66182
|
-
sessionKey: params.sessionKey,
|
|
66183
|
-
sessionId,
|
|
66184
|
-
workspacePath: params.workspacePath,
|
|
66185
|
-
runnerCommand: params.runnerCommand,
|
|
66186
|
-
lastAdmittedPromptAt: existing?.lastAdmittedPromptAt,
|
|
66187
|
-
followUp: existing?.followUp,
|
|
66188
|
-
runtime: existing?.runtime,
|
|
66189
|
-
loops: existing?.loops ?? existing?.intervalLoops,
|
|
66190
|
-
queues: existing?.queues,
|
|
66191
|
-
recentConversation: existing?.recentConversation,
|
|
66192
|
-
updatedAt: Date.now()
|
|
66193
|
-
});
|
|
66194
|
-
}
|
|
66195
66397
|
async withPathLock(work) {
|
|
66196
66398
|
const previous = SessionStore.pathLocks.get(this.storePath) ?? Promise.resolve();
|
|
66197
66399
|
let release;
|
|
@@ -66348,9 +66550,7 @@ class AgentSessionState {
|
|
|
66348
66550
|
lastAdmittedPromptAt: existing?.lastAdmittedPromptAt,
|
|
66349
66551
|
followUp: existing?.followUp,
|
|
66350
66552
|
runnerCommand: params.runnerCommand ?? existing?.runnerCommand ?? resolved.runner.command,
|
|
66351
|
-
runtime: {
|
|
66352
|
-
state: "idle"
|
|
66353
|
-
},
|
|
66553
|
+
runtime: params.preserveRuntime ? existing?.runtime : { state: "idle" },
|
|
66354
66554
|
loops: getStoredLoops(existing),
|
|
66355
66555
|
recentConversation: existing?.recentConversation
|
|
66356
66556
|
}));
|
|
@@ -66394,19 +66594,6 @@ class AgentSessionState {
|
|
|
66394
66594
|
agentId: target.agentId
|
|
66395
66595
|
};
|
|
66396
66596
|
}
|
|
66397
|
-
async listActiveSessionRuntimes() {
|
|
66398
|
-
const entries = await this.sessionStore.list();
|
|
66399
|
-
return entries.filter(hasActiveRuntime).map((entry) => ({
|
|
66400
|
-
state: entry.runtime.state,
|
|
66401
|
-
startedAt: entry.runtime.startedAt,
|
|
66402
|
-
detachedAt: entry.runtime.detachedAt,
|
|
66403
|
-
finalReplyAt: entry.runtime.finalReplyAt,
|
|
66404
|
-
lastMessageToolReplyAt: entry.runtime.lastMessageToolReplyAt,
|
|
66405
|
-
messageToolFinalReplyAt: entry.runtime.messageToolFinalReplyAt,
|
|
66406
|
-
sessionKey: entry.sessionKey,
|
|
66407
|
-
agentId: entry.agentId
|
|
66408
|
-
}));
|
|
66409
|
-
}
|
|
66410
66597
|
async listIntervalLoops(params) {
|
|
66411
66598
|
const entries = await this.sessionStore.list();
|
|
66412
66599
|
return entries.flatMap((entry) => getStoredLoops(entry).filter((loop) => !params?.sessionKey || entry.sessionKey === params.sessionKey).map((loop) => ({
|
|
@@ -66664,6 +66851,10 @@ class AgentSessionState {
|
|
|
66664
66851
|
const entry = await this.sessionStore.get(sessionKey);
|
|
66665
66852
|
return getStoredQueues(entry).some((item) => item.status === "pending" || item.status === "running");
|
|
66666
66853
|
}
|
|
66854
|
+
async hasQueuedItem(sessionKey, queueId) {
|
|
66855
|
+
const entry = await this.sessionStore.get(sessionKey);
|
|
66856
|
+
return getStoredQueues(entry).some((item) => item.id === queueId);
|
|
66857
|
+
}
|
|
66667
66858
|
async resetStaleRunningQueuedItems(activeSessionKeys) {
|
|
66668
66859
|
const entries = await this.sessionStore.list();
|
|
66669
66860
|
let reset = 0;
|
|
@@ -66744,11 +66935,74 @@ function getStoredLoops(entry) {
|
|
|
66744
66935
|
function getStoredQueues(entry) {
|
|
66745
66936
|
return entry?.queues ?? [];
|
|
66746
66937
|
}
|
|
66747
|
-
|
|
66748
|
-
|
|
66938
|
+
|
|
66939
|
+
// src/agents/session-identity.ts
|
|
66940
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
66941
|
+
function createSessionId() {
|
|
66942
|
+
return randomUUID3();
|
|
66943
|
+
}
|
|
66944
|
+
function parseRunnerSessionId(snapshot, pattern) {
|
|
66945
|
+
const regex = new RegExp(pattern, "ig");
|
|
66946
|
+
let lastMatch = null;
|
|
66947
|
+
for (;; ) {
|
|
66948
|
+
const match = regex.exec(snapshot);
|
|
66949
|
+
if (!match) {
|
|
66950
|
+
break;
|
|
66951
|
+
}
|
|
66952
|
+
if (!match[0]) {
|
|
66953
|
+
break;
|
|
66954
|
+
}
|
|
66955
|
+
lastMatch = match;
|
|
66956
|
+
}
|
|
66957
|
+
return (lastMatch?.[1] ?? lastMatch?.[0] ?? "").trim() || null;
|
|
66958
|
+
}
|
|
66959
|
+
|
|
66960
|
+
// src/agents/session-mapping.ts
|
|
66961
|
+
class SessionMapping {
|
|
66962
|
+
sessionState;
|
|
66963
|
+
constructor(sessionState) {
|
|
66964
|
+
this.sessionState = sessionState;
|
|
66965
|
+
}
|
|
66966
|
+
async get(sessionKey) {
|
|
66967
|
+
return this.sessionState.getEntry(sessionKey);
|
|
66968
|
+
}
|
|
66969
|
+
async prepareStartup(resolved) {
|
|
66970
|
+
const entry = await this.sessionState.getEntry(resolved.sessionKey);
|
|
66971
|
+
const storedSessionId = entry?.sessionId?.trim() || undefined;
|
|
66972
|
+
if (storedSessionId) {
|
|
66973
|
+
return {
|
|
66974
|
+
storedSessionId,
|
|
66975
|
+
sessionId: storedSessionId,
|
|
66976
|
+
resume: true
|
|
66977
|
+
};
|
|
66978
|
+
}
|
|
66979
|
+
if (resolved.runner.sessionId.create.mode !== "explicit") {
|
|
66980
|
+
return {
|
|
66981
|
+
resume: false
|
|
66982
|
+
};
|
|
66983
|
+
}
|
|
66984
|
+
return {
|
|
66985
|
+
sessionId: createSessionId(),
|
|
66986
|
+
resume: false
|
|
66987
|
+
};
|
|
66988
|
+
}
|
|
66989
|
+
async setActive(resolved, params) {
|
|
66990
|
+
return this.sessionState.touchSessionEntry(resolved, {
|
|
66991
|
+
sessionId: params.sessionId,
|
|
66992
|
+
runnerCommand: params.runnerCommand,
|
|
66993
|
+
runtime: params.runtime
|
|
66994
|
+
});
|
|
66995
|
+
}
|
|
66996
|
+
async clearActive(resolved, params = {}) {
|
|
66997
|
+
return this.sessionState.clearSessionIdEntry(resolved, params);
|
|
66998
|
+
}
|
|
66999
|
+
async touch(resolved, params = {}) {
|
|
67000
|
+
return this.sessionState.touchSessionEntry(resolved, params);
|
|
67001
|
+
}
|
|
66749
67002
|
}
|
|
66750
67003
|
|
|
66751
67004
|
// src/agents/session-key.ts
|
|
67005
|
+
import { createHash } from "node:crypto";
|
|
66752
67006
|
var DEFAULT_MAIN_KEY = "main";
|
|
66753
67007
|
var DEFAULT_BOT_ID = "default";
|
|
66754
67008
|
var DEFAULT_ACCOUNT_ID = DEFAULT_BOT_ID;
|
|
@@ -66839,7 +67093,8 @@ function buildTmuxSessionName(params) {
|
|
|
66839
67093
|
mainKey: normalizeMainKey(params.mainKey)
|
|
66840
67094
|
});
|
|
66841
67095
|
const baseName = sanitizeSessionName(rendered);
|
|
66842
|
-
|
|
67096
|
+
const sessionHash = createHash("sha1").update(params.sessionKey).digest("hex").slice(0, 8);
|
|
67097
|
+
return `${baseName}-${sessionHash}`;
|
|
66843
67098
|
}
|
|
66844
67099
|
|
|
66845
67100
|
// src/agents/resolved-target.ts
|
|
@@ -66999,39 +67254,15 @@ class AgentJobQueue {
|
|
|
66999
67254
|
if (!state) {
|
|
67000
67255
|
return 0;
|
|
67001
67256
|
}
|
|
67002
|
-
|
|
67003
|
-
const removedEntries = state.entries.filter((entry) => entry.status === "pending");
|
|
67004
|
-
state.entries = keptEntries;
|
|
67005
|
-
for (const entry of removedEntries) {
|
|
67006
|
-
Promise.resolve(entry.lifecycle?.onClear?.()).catch(() => {
|
|
67007
|
-
return;
|
|
67008
|
-
});
|
|
67009
|
-
entry.reject(new ClearedQueuedTaskError);
|
|
67010
|
-
}
|
|
67011
|
-
if (state.entries.length === 0 && !state.running) {
|
|
67012
|
-
this.states.delete(key);
|
|
67013
|
-
}
|
|
67014
|
-
return removedEntries.length;
|
|
67257
|
+
return this.clearPendingEntriesForState(key, state, () => true);
|
|
67015
67258
|
}
|
|
67016
|
-
|
|
67017
|
-
const
|
|
67018
|
-
|
|
67019
|
-
|
|
67020
|
-
const keptEntries = state.entries.filter((entry) => entry.status === "running" || !idSet.has(entry.id));
|
|
67021
|
-
const removedEntries = state.entries.filter((entry) => entry.status === "pending" && idSet.has(entry.id));
|
|
67022
|
-
state.entries = keptEntries;
|
|
67023
|
-
cleared += removedEntries.length;
|
|
67024
|
-
for (const entry of removedEntries) {
|
|
67025
|
-
Promise.resolve(entry.lifecycle?.onClear?.()).catch(() => {
|
|
67026
|
-
return;
|
|
67027
|
-
});
|
|
67028
|
-
entry.reject(new ClearedQueuedTaskError);
|
|
67029
|
-
}
|
|
67030
|
-
if (state.entries.length === 0 && !state.running) {
|
|
67031
|
-
this.states.delete(key);
|
|
67032
|
-
}
|
|
67259
|
+
clearPendingByIdsForKey(key, ids) {
|
|
67260
|
+
const state = this.states.get(key);
|
|
67261
|
+
if (!state) {
|
|
67262
|
+
return 0;
|
|
67033
67263
|
}
|
|
67034
|
-
|
|
67264
|
+
const idSet = new Set(ids);
|
|
67265
|
+
return this.clearPendingEntriesForState(key, state, (entry) => idSet.has(entry.id));
|
|
67035
67266
|
}
|
|
67036
67267
|
getOrCreateState(key) {
|
|
67037
67268
|
const existing = this.states.get(key);
|
|
@@ -67056,6 +67287,21 @@ class AgentJobQueue {
|
|
|
67056
67287
|
return left.sequence - right.sequence;
|
|
67057
67288
|
});
|
|
67058
67289
|
}
|
|
67290
|
+
clearPendingEntriesForState(key, state, shouldClear) {
|
|
67291
|
+
const keptEntries = state.entries.filter((entry) => entry.status === "running" || !shouldClear(entry));
|
|
67292
|
+
const removedEntries = state.entries.filter((entry) => entry.status === "pending" && shouldClear(entry));
|
|
67293
|
+
state.entries = keptEntries;
|
|
67294
|
+
for (const entry of removedEntries) {
|
|
67295
|
+
Promise.resolve(entry.lifecycle?.onClear?.()).catch(() => {
|
|
67296
|
+
return;
|
|
67297
|
+
});
|
|
67298
|
+
entry.reject(new ClearedQueuedTaskError);
|
|
67299
|
+
}
|
|
67300
|
+
if (state.entries.length === 0 && !state.running) {
|
|
67301
|
+
this.states.delete(key);
|
|
67302
|
+
}
|
|
67303
|
+
return removedEntries.length;
|
|
67304
|
+
}
|
|
67059
67305
|
async drain(key, state) {
|
|
67060
67306
|
if (state.running) {
|
|
67061
67307
|
return;
|
|
@@ -67142,10 +67388,10 @@ function buildResumeCommandPreview(resolved, sessionId) {
|
|
|
67142
67388
|
}
|
|
67143
67389
|
|
|
67144
67390
|
// src/agents/loop-control-shared.ts
|
|
67145
|
-
import { randomUUID as
|
|
67391
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
67146
67392
|
import { join as join10 } from "node:path";
|
|
67147
67393
|
function createLoopId() {
|
|
67148
|
-
return
|
|
67394
|
+
return randomUUID4().split("-")[0] ?? randomUUID4();
|
|
67149
67395
|
}
|
|
67150
67396
|
function buildStoredLoopSender(params) {
|
|
67151
67397
|
const providerId = params.providerId.trim();
|
|
@@ -67172,10 +67418,11 @@ function createStoredLoopBase(params) {
|
|
|
67172
67418
|
updatedAt: now,
|
|
67173
67419
|
nextRunAt: params.nextRunAt,
|
|
67174
67420
|
promptText: params.promptText,
|
|
67175
|
-
canonicalPromptText: params.canonicalPromptText,
|
|
67176
67421
|
protectedControlMutationRule: params.protectedControlMutationRule,
|
|
67177
67422
|
promptSummary: params.promptSummary,
|
|
67178
67423
|
promptSource: params.promptSource,
|
|
67424
|
+
progressMessages: params.progressMessages,
|
|
67425
|
+
loopStart: params.loopStart,
|
|
67179
67426
|
createdBy: params.createdBy,
|
|
67180
67427
|
sender: params.sender ?? deriveLegacyLoopSender({
|
|
67181
67428
|
createdBy: params.createdBy,
|
|
@@ -67202,10 +67449,11 @@ function createStoredIntervalLoop(params) {
|
|
|
67202
67449
|
...createStoredLoopBase({
|
|
67203
67450
|
nextRunAt: Date.now(),
|
|
67204
67451
|
promptText: params.promptText,
|
|
67205
|
-
canonicalPromptText: params.canonicalPromptText,
|
|
67206
67452
|
protectedControlMutationRule: params.protectedControlMutationRule,
|
|
67207
67453
|
promptSummary: params.promptSummary,
|
|
67208
67454
|
promptSource: params.promptSource,
|
|
67455
|
+
progressMessages: params.progressMessages,
|
|
67456
|
+
loopStart: params.loopStart,
|
|
67209
67457
|
createdBy: params.createdBy,
|
|
67210
67458
|
sender: params.sender,
|
|
67211
67459
|
surfaceBinding: params.surfaceBinding,
|
|
@@ -67232,10 +67480,11 @@ function createStoredCalendarLoop(params) {
|
|
|
67232
67480
|
...createStoredLoopBase({
|
|
67233
67481
|
nextRunAt,
|
|
67234
67482
|
promptText: params.promptText,
|
|
67235
|
-
canonicalPromptText: params.canonicalPromptText,
|
|
67236
67483
|
protectedControlMutationRule: params.protectedControlMutationRule,
|
|
67237
67484
|
promptSummary: params.promptSummary,
|
|
67238
67485
|
promptSource: params.promptSource,
|
|
67486
|
+
progressMessages: params.progressMessages,
|
|
67487
|
+
loopStart: params.loopStart,
|
|
67239
67488
|
createdBy: params.createdBy,
|
|
67240
67489
|
sender: params.sender,
|
|
67241
67490
|
surfaceBinding: params.surfaceBinding,
|
|
@@ -67392,87 +67641,86 @@ class ManagedLoopController {
|
|
|
67392
67641
|
}
|
|
67393
67642
|
async reconcilePersistedIntervalLoops() {
|
|
67394
67643
|
const persistedLoops = await this.deps.sessionState.listIntervalLoops();
|
|
67395
|
-
const
|
|
67644
|
+
const persistedKeys = new Set;
|
|
67396
67645
|
for (const persisted of persistedLoops) {
|
|
67397
|
-
|
|
67646
|
+
const managedKey = this.buildManagedLoopKey(persisted.sessionKey, persisted.id);
|
|
67647
|
+
persistedKeys.add(managedKey);
|
|
67398
67648
|
if (persisted.attemptedRuns >= persisted.maxRuns) {
|
|
67399
|
-
this.dropManagedIntervalLoop(
|
|
67649
|
+
this.dropManagedIntervalLoop(managedKey);
|
|
67400
67650
|
continue;
|
|
67401
67651
|
}
|
|
67402
|
-
if (this.intervalLoops.has(
|
|
67652
|
+
if (this.intervalLoops.has(managedKey)) {
|
|
67403
67653
|
continue;
|
|
67404
67654
|
}
|
|
67405
|
-
this.intervalLoops.set(
|
|
67655
|
+
this.intervalLoops.set(managedKey, {
|
|
67406
67656
|
target: {
|
|
67407
67657
|
agentId: persisted.agentId,
|
|
67408
67658
|
sessionKey: persisted.sessionKey
|
|
67409
67659
|
},
|
|
67410
67660
|
loop: persisted
|
|
67411
67661
|
});
|
|
67412
|
-
this.scheduleIntervalLoopTimer(
|
|
67662
|
+
this.scheduleIntervalLoopTimer(managedKey, Math.max(0, persisted.nextRunAt - Date.now()));
|
|
67413
67663
|
}
|
|
67414
|
-
for (const
|
|
67415
|
-
if (!
|
|
67416
|
-
this.dropManagedIntervalLoop(
|
|
67664
|
+
for (const managedKey of this.intervalLoops.keys()) {
|
|
67665
|
+
if (!persistedKeys.has(managedKey)) {
|
|
67666
|
+
this.dropManagedIntervalLoop(managedKey);
|
|
67417
67667
|
}
|
|
67418
67668
|
}
|
|
67419
67669
|
}
|
|
67420
67670
|
async createIntervalLoop(params) {
|
|
67421
67671
|
this.assertActiveLoopCapacity();
|
|
67422
67672
|
const loop = createStoredIntervalLoop(params);
|
|
67673
|
+
const managedKey = this.buildManagedLoopKey(params.target.sessionKey, loop.id);
|
|
67423
67674
|
await this.deps.sessionState.setIntervalLoop(this.deps.resolveTarget(params.target), loop);
|
|
67424
|
-
this.intervalLoops.set(
|
|
67675
|
+
this.intervalLoops.set(managedKey, {
|
|
67425
67676
|
target: params.target,
|
|
67426
67677
|
loop
|
|
67427
67678
|
});
|
|
67428
|
-
await this.runIntervalLoopIteration(
|
|
67679
|
+
await this.runIntervalLoopIteration(managedKey, {
|
|
67429
67680
|
notifyStart: false
|
|
67430
67681
|
});
|
|
67431
|
-
return this.getIntervalLoop(loop.id);
|
|
67682
|
+
return this.getIntervalLoop(params.target.sessionKey, loop.id);
|
|
67432
67683
|
}
|
|
67433
67684
|
async createCalendarLoop(params) {
|
|
67434
67685
|
this.assertActiveLoopCapacity();
|
|
67435
67686
|
const loop = createStoredCalendarLoop(params);
|
|
67687
|
+
const managedKey = this.buildManagedLoopKey(params.target.sessionKey, loop.id);
|
|
67436
67688
|
await this.deps.sessionState.setIntervalLoop(this.deps.resolveTarget(params.target), loop);
|
|
67437
|
-
this.intervalLoops.set(
|
|
67689
|
+
this.intervalLoops.set(managedKey, {
|
|
67438
67690
|
target: params.target,
|
|
67439
67691
|
loop
|
|
67440
67692
|
});
|
|
67441
|
-
this.scheduleIntervalLoopTimer(
|
|
67442
|
-
return this.getIntervalLoop(loop.id);
|
|
67693
|
+
this.scheduleIntervalLoopTimer(managedKey, Math.max(0, loop.nextRunAt - Date.now()));
|
|
67694
|
+
return this.getIntervalLoop(params.target.sessionKey, loop.id);
|
|
67443
67695
|
}
|
|
67444
|
-
async cancelIntervalLoop(loopId) {
|
|
67445
|
-
const
|
|
67696
|
+
async cancelIntervalLoop(target, loopId) {
|
|
67697
|
+
const managedKey = this.buildManagedLoopKey(target.sessionKey, loopId);
|
|
67698
|
+
const managed = this.intervalLoops.get(managedKey);
|
|
67446
67699
|
if (!managed) {
|
|
67447
67700
|
return false;
|
|
67448
67701
|
}
|
|
67449
|
-
|
|
67450
|
-
clearTimeout(managed.timer);
|
|
67451
|
-
this.loopTimers.delete(managed.timer);
|
|
67452
|
-
}
|
|
67453
|
-
this.intervalLoops.delete(loopId);
|
|
67454
|
-
await this.deps.sessionState.removeIntervalLoop(this.deps.resolveTarget(managed.target), loopId);
|
|
67702
|
+
await this.cancelManagedIntervalLoop(managedKey, managed);
|
|
67455
67703
|
return true;
|
|
67456
67704
|
}
|
|
67457
67705
|
async cancelIntervalLoopsForSession(target) {
|
|
67458
|
-
const matching = [...this.intervalLoops.
|
|
67459
|
-
for (const
|
|
67460
|
-
await this.
|
|
67706
|
+
const matching = [...this.intervalLoops.entries()].filter(([, managed]) => managed.target.sessionKey === target.sessionKey);
|
|
67707
|
+
for (const [managedKey, managed] of matching) {
|
|
67708
|
+
await this.cancelManagedIntervalLoop(managedKey, managed);
|
|
67461
67709
|
}
|
|
67462
67710
|
return matching.length;
|
|
67463
67711
|
}
|
|
67464
67712
|
async cancelAllIntervalLoops() {
|
|
67465
|
-
const
|
|
67466
|
-
for (const
|
|
67467
|
-
await this.
|
|
67713
|
+
const matching = [...this.intervalLoops.entries()];
|
|
67714
|
+
for (const [managedKey, managed] of matching) {
|
|
67715
|
+
await this.cancelManagedIntervalLoop(managedKey, managed);
|
|
67468
67716
|
}
|
|
67469
|
-
return
|
|
67717
|
+
return matching.length;
|
|
67470
67718
|
}
|
|
67471
67719
|
listIntervalLoops(params) {
|
|
67472
67720
|
return [...this.intervalLoops.values()].filter((managed) => !params?.sessionKey || managed.target.sessionKey === params.sessionKey).map((managed) => this.toLoopStatus(managed)).sort((left, right) => left.nextRunAt - right.nextRunAt);
|
|
67473
67721
|
}
|
|
67474
|
-
getIntervalLoop(loopId) {
|
|
67475
|
-
const managed = this.intervalLoops.get(loopId);
|
|
67722
|
+
getIntervalLoop(sessionKey, loopId) {
|
|
67723
|
+
const managed = this.intervalLoops.get(this.buildManagedLoopKey(sessionKey, loopId));
|
|
67476
67724
|
return managed ? this.toLoopStatus(managed) : null;
|
|
67477
67725
|
}
|
|
67478
67726
|
getActiveIntervalLoopCount() {
|
|
@@ -67496,8 +67744,8 @@ class ManagedLoopController {
|
|
|
67496
67744
|
const entry = await this.deps.sessionState.getEntry(managed.target.sessionKey);
|
|
67497
67745
|
return (entry?.loops ?? entry?.intervalLoops ?? []).some((loop) => loop.id === managed.loop.id);
|
|
67498
67746
|
}
|
|
67499
|
-
dropManagedIntervalLoop(
|
|
67500
|
-
const managed = this.intervalLoops.get(
|
|
67747
|
+
dropManagedIntervalLoop(managedKey) {
|
|
67748
|
+
const managed = this.intervalLoops.get(managedKey);
|
|
67501
67749
|
if (!managed) {
|
|
67502
67750
|
return;
|
|
67503
67751
|
}
|
|
@@ -67505,30 +67753,34 @@ class ManagedLoopController {
|
|
|
67505
67753
|
clearTimeout(managed.timer);
|
|
67506
67754
|
this.loopTimers.delete(managed.timer);
|
|
67507
67755
|
}
|
|
67508
|
-
this.intervalLoops.delete(
|
|
67756
|
+
this.intervalLoops.delete(managedKey);
|
|
67757
|
+
}
|
|
67758
|
+
async cancelManagedIntervalLoop(managedKey, managed) {
|
|
67759
|
+
this.dropManagedIntervalLoop(managedKey);
|
|
67760
|
+
await this.deps.sessionState.removeIntervalLoop(this.deps.resolveTarget(managed.target), managed.loop.id);
|
|
67509
67761
|
}
|
|
67510
|
-
async runIntervalLoopIteration(
|
|
67511
|
-
const managed = this.intervalLoops.get(
|
|
67762
|
+
async runIntervalLoopIteration(managedKey, options = {}) {
|
|
67763
|
+
const managed = this.intervalLoops.get(managedKey);
|
|
67512
67764
|
if (!managed) {
|
|
67513
67765
|
return;
|
|
67514
67766
|
}
|
|
67515
67767
|
if (!await this.isManagedLoopPersisted(managed)) {
|
|
67516
|
-
this.dropManagedIntervalLoop(
|
|
67768
|
+
this.dropManagedIntervalLoop(managedKey);
|
|
67517
67769
|
return;
|
|
67518
67770
|
}
|
|
67519
67771
|
const attemptedRuns = managed.loop.attemptedRuns + 1;
|
|
67520
67772
|
const now = Date.now();
|
|
67521
67773
|
const nextLoopState = this.buildNextLoopState(managed.loop, attemptedRuns, now);
|
|
67522
67774
|
if (await this.deps.isSessionBusy(managed.target)) {
|
|
67523
|
-
await this.skipLoopIteration(
|
|
67775
|
+
await this.skipLoopIteration(managedKey, managed, nextLoopState, attemptedRuns, now);
|
|
67524
67776
|
return;
|
|
67525
67777
|
}
|
|
67526
67778
|
nextLoopState.executedRuns += 1;
|
|
67527
67779
|
if (!await this.updateManagedIntervalLoop(managed, nextLoopState)) {
|
|
67528
|
-
this.dropManagedIntervalLoop(
|
|
67780
|
+
this.dropManagedIntervalLoop(managedKey);
|
|
67529
67781
|
return;
|
|
67530
67782
|
}
|
|
67531
|
-
await this.executeLoopIteration(
|
|
67783
|
+
await this.executeLoopIteration(managedKey, managed.target, nextLoopState, attemptedRuns, now, options.notifyStart !== false);
|
|
67532
67784
|
}
|
|
67533
67785
|
buildNextLoopState(loop, attemptedRuns, now) {
|
|
67534
67786
|
return {
|
|
@@ -67538,33 +67790,35 @@ class ManagedLoopController {
|
|
|
67538
67790
|
nextRunAt: this.computeNextManagedLoopRunAtMs(loop, now)
|
|
67539
67791
|
};
|
|
67540
67792
|
}
|
|
67541
|
-
async skipLoopIteration(
|
|
67793
|
+
async skipLoopIteration(managedKey, managed, nextLoopState, attemptedRuns, now) {
|
|
67542
67794
|
nextLoopState.skippedRuns += 1;
|
|
67543
67795
|
if (!await this.updateManagedIntervalLoop(managed, nextLoopState)) {
|
|
67544
|
-
this.dropManagedIntervalLoop(
|
|
67796
|
+
this.dropManagedIntervalLoop(managedKey);
|
|
67545
67797
|
return;
|
|
67546
67798
|
}
|
|
67547
67799
|
if (attemptedRuns >= managed.loop.maxRuns) {
|
|
67548
|
-
|
|
67800
|
+
this.dropManagedIntervalLoop(managedKey);
|
|
67801
|
+
await this.deps.sessionState.removeIntervalLoop(this.deps.resolveTarget(managed.target), managed.loop.id);
|
|
67549
67802
|
return;
|
|
67550
67803
|
}
|
|
67551
|
-
this.scheduleIntervalLoopTimer(
|
|
67804
|
+
this.scheduleIntervalLoopTimer(managedKey, Math.max(0, nextLoopState.nextRunAt - now));
|
|
67552
67805
|
}
|
|
67553
|
-
async executeLoopIteration(
|
|
67554
|
-
await this.notifyAndEnqueueLoop(
|
|
67806
|
+
async executeLoopIteration(managedKey, target, nextLoopState, attemptedRuns, now, notifyStart) {
|
|
67807
|
+
await this.notifyAndEnqueueLoop(managedKey, target, nextLoopState, attemptedRuns, notifyStart);
|
|
67555
67808
|
if (attemptedRuns >= nextLoopState.maxRuns) {
|
|
67556
|
-
|
|
67809
|
+
this.dropManagedIntervalLoop(managedKey);
|
|
67810
|
+
await this.deps.sessionState.removeIntervalLoop(this.deps.resolveTarget(target), nextLoopState.id);
|
|
67557
67811
|
return;
|
|
67558
67812
|
}
|
|
67559
|
-
this.scheduleIntervalLoopTimer(
|
|
67813
|
+
this.scheduleIntervalLoopTimer(managedKey, Math.max(0, nextLoopState.nextRunAt - now));
|
|
67560
67814
|
}
|
|
67561
|
-
async notifyAndEnqueueLoop(
|
|
67815
|
+
async notifyAndEnqueueLoop(managedKey, target, nextLoopState, attemptedRuns, notifyStart) {
|
|
67562
67816
|
if (notifyStart) {
|
|
67563
67817
|
await this.deps.surfaceRuntime.notifyManagedLoopStart(target, nextLoopState);
|
|
67564
67818
|
}
|
|
67565
67819
|
const promptText = await this.deps.surfaceRuntime.buildManagedLoopPrompt(target.agentId, nextLoopState);
|
|
67566
67820
|
const { result } = this.deps.enqueuePrompt(target, promptText, {
|
|
67567
|
-
observerId: `loop:${
|
|
67821
|
+
observerId: `loop:${managedKey}:${attemptedRuns}`,
|
|
67568
67822
|
onUpdate: async () => {
|
|
67569
67823
|
return;
|
|
67570
67824
|
}
|
|
@@ -67575,8 +67829,8 @@ class ManagedLoopController {
|
|
|
67575
67829
|
}
|
|
67576
67830
|
});
|
|
67577
67831
|
}
|
|
67578
|
-
scheduleIntervalLoopTimer(
|
|
67579
|
-
const managed = this.intervalLoops.get(
|
|
67832
|
+
scheduleIntervalLoopTimer(managedKey, delayMs) {
|
|
67833
|
+
const managed = this.intervalLoops.get(managedKey);
|
|
67580
67834
|
if (!managed) {
|
|
67581
67835
|
return;
|
|
67582
67836
|
}
|
|
@@ -67586,12 +67840,12 @@ class ManagedLoopController {
|
|
|
67586
67840
|
}
|
|
67587
67841
|
const timer = setTimeout(() => {
|
|
67588
67842
|
this.loopTimers.delete(timer);
|
|
67589
|
-
const current = this.intervalLoops.get(
|
|
67843
|
+
const current = this.intervalLoops.get(managedKey);
|
|
67590
67844
|
if (!current) {
|
|
67591
67845
|
return;
|
|
67592
67846
|
}
|
|
67593
67847
|
current.timer = undefined;
|
|
67594
|
-
this.runIntervalLoopIteration(
|
|
67848
|
+
this.runIntervalLoopIteration(managedKey, { notifyStart: true }).catch((error) => {
|
|
67595
67849
|
if (!this.deps.shouldSuppressShutdownError(error)) {
|
|
67596
67850
|
console.error("loop execution failed", error);
|
|
67597
67851
|
}
|
|
@@ -67608,6 +67862,9 @@ class ManagedLoopController {
|
|
|
67608
67862
|
managed.loop = nextLoopState;
|
|
67609
67863
|
return true;
|
|
67610
67864
|
}
|
|
67865
|
+
buildManagedLoopKey(sessionKey, loopId) {
|
|
67866
|
+
return `${sessionKey}::${loopId}`;
|
|
67867
|
+
}
|
|
67611
67868
|
computeNextManagedLoopRunAtMs(loop, nowMs) {
|
|
67612
67869
|
if (loop.kind === "calendar") {
|
|
67613
67870
|
return computeNextCalendarLoopRunAtMs({
|
|
@@ -67624,6 +67881,8 @@ class ManagedLoopController {
|
|
|
67624
67881
|
}
|
|
67625
67882
|
|
|
67626
67883
|
// src/agents/managed-queue-controller.ts
|
|
67884
|
+
var QUEUE_MESSAGE_TOOL_FINAL_POLL_MS = 250;
|
|
67885
|
+
|
|
67627
67886
|
class ManagedQueueController {
|
|
67628
67887
|
deps;
|
|
67629
67888
|
queuedItems = new Map;
|
|
@@ -67646,7 +67905,7 @@ class ManagedQueueController {
|
|
|
67646
67905
|
}
|
|
67647
67906
|
async clearQueuedPrompts(target) {
|
|
67648
67907
|
const clearedStored = await this.deps.sessionState.clearPendingQueuedItemsForSessionKey(target.sessionKey);
|
|
67649
|
-
this.deps.queue.
|
|
67908
|
+
this.deps.queue.clearPendingByIdsForKey(target.sessionKey, clearedStored.map((item) => item.id));
|
|
67650
67909
|
return clearedStored.length;
|
|
67651
67910
|
}
|
|
67652
67911
|
enqueuePrompt(target, prompt, callbacks) {
|
|
@@ -67684,8 +67943,8 @@ class ManagedQueueController {
|
|
|
67684
67943
|
return;
|
|
67685
67944
|
}
|
|
67686
67945
|
this.setManagedQueueItem(target, item, true);
|
|
67687
|
-
return this.persistQueueItem(this.deps.resolveTarget(target), item).then(() => this.markPersisted(item)).catch((error) => {
|
|
67688
|
-
this.clearPendingById(item);
|
|
67946
|
+
return this.persistQueueItem(this.deps.resolveTarget(target), item).then(() => this.markPersisted(target, item)).catch((error) => {
|
|
67947
|
+
this.clearPendingById(target, item);
|
|
67689
67948
|
throw error;
|
|
67690
67949
|
});
|
|
67691
67950
|
}
|
|
@@ -67696,25 +67955,58 @@ class ManagedQueueController {
|
|
|
67696
67955
|
await reconciledBeforeStart;
|
|
67697
67956
|
return !await this.deps.hasBlockingActiveRun(target);
|
|
67698
67957
|
}
|
|
67958
|
+
async canStartPersistedQueueItem(target, item) {
|
|
67959
|
+
if (await this.deps.hasBlockingActiveRun(target)) {
|
|
67960
|
+
return false;
|
|
67961
|
+
}
|
|
67962
|
+
if (!await this.deps.sessionState.hasQueuedItem(target.sessionKey, item.id)) {
|
|
67963
|
+
this.clearPendingById(target, item);
|
|
67964
|
+
return false;
|
|
67965
|
+
}
|
|
67966
|
+
return true;
|
|
67967
|
+
}
|
|
67699
67968
|
async reconcilePersistedQueueItems() {
|
|
67700
67969
|
const persistedItems = await this.deps.sessionState.listQueuedItems({
|
|
67701
67970
|
statuses: ["pending", "running"]
|
|
67702
67971
|
});
|
|
67703
|
-
const
|
|
67704
|
-
const
|
|
67705
|
-
if (
|
|
67706
|
-
|
|
67707
|
-
for (const
|
|
67708
|
-
|
|
67972
|
+
const persistedKeys = new Set(persistedItems.map((item) => this.buildManagedQueueKey(item.sessionKey, item.id)));
|
|
67973
|
+
const removedManagedItems = [...this.queuedItems.entries()].filter(([key, managed]) => !managed.persisting && !persistedKeys.has(key));
|
|
67974
|
+
if (removedManagedItems.length > 0) {
|
|
67975
|
+
const removedBySession = new Map;
|
|
67976
|
+
for (const [key, managed] of removedManagedItems) {
|
|
67977
|
+
const ids = removedBySession.get(managed.target.sessionKey) ?? [];
|
|
67978
|
+
ids.push(managed.item.id);
|
|
67979
|
+
removedBySession.set(managed.target.sessionKey, ids);
|
|
67980
|
+
this.queuedItems.delete(key);
|
|
67981
|
+
}
|
|
67982
|
+
for (const [sessionKey, itemIds] of removedBySession.entries()) {
|
|
67983
|
+
this.deps.queue.clearPendingByIdsForKey(sessionKey, itemIds);
|
|
67709
67984
|
}
|
|
67710
67985
|
}
|
|
67711
67986
|
for (const persisted of persistedItems) {
|
|
67712
|
-
if (persisted.status
|
|
67987
|
+
if (persisted.status === "running") {
|
|
67988
|
+
await this.clearStaleRunningQueueItem(persisted);
|
|
67989
|
+
continue;
|
|
67990
|
+
}
|
|
67991
|
+
if (this.queuedItems.has(this.buildManagedQueueKey(persisted.sessionKey, persisted.id))) {
|
|
67713
67992
|
continue;
|
|
67714
67993
|
}
|
|
67715
67994
|
this.enqueuePersistedQueueItem(persisted);
|
|
67716
67995
|
}
|
|
67717
67996
|
}
|
|
67997
|
+
async clearStaleRunningQueueItem(item) {
|
|
67998
|
+
if (this.queuedItems.has(this.buildManagedQueueKey(item.sessionKey, item.id))) {
|
|
67999
|
+
return;
|
|
68000
|
+
}
|
|
68001
|
+
const target = {
|
|
68002
|
+
agentId: item.agentId,
|
|
68003
|
+
sessionKey: item.sessionKey
|
|
68004
|
+
};
|
|
68005
|
+
if (await this.deps.hasBlockingActiveRun(target)) {
|
|
68006
|
+
return;
|
|
68007
|
+
}
|
|
68008
|
+
await this.removeManagedQueueItem(target, item);
|
|
68009
|
+
}
|
|
67718
68010
|
persistQueueItem(resolved, item) {
|
|
67719
68011
|
return this.deps.sessionState.countPendingQueuedItemsForSessionKey(resolved.sessionKey, {
|
|
67720
68012
|
excludeId: item.id
|
|
@@ -67727,21 +68019,22 @@ class ManagedQueueController {
|
|
|
67727
68019
|
});
|
|
67728
68020
|
}
|
|
67729
68021
|
setManagedQueueItem(target, item, persisting) {
|
|
67730
|
-
this.queuedItems.set(item.id, {
|
|
68022
|
+
this.queuedItems.set(this.buildManagedQueueKey(target.sessionKey, item.id), {
|
|
67731
68023
|
target,
|
|
67732
68024
|
item,
|
|
67733
68025
|
persisting
|
|
67734
68026
|
});
|
|
67735
68027
|
}
|
|
67736
|
-
markPersisted(item) {
|
|
67737
|
-
const
|
|
68028
|
+
markPersisted(target, item) {
|
|
68029
|
+
const key = this.buildManagedQueueKey(target.sessionKey, item.id);
|
|
68030
|
+
const managed = this.queuedItems.get(key);
|
|
67738
68031
|
if (managed) {
|
|
67739
|
-
this.queuedItems.set(
|
|
68032
|
+
this.queuedItems.set(key, { ...managed, persisting: false });
|
|
67740
68033
|
}
|
|
67741
68034
|
}
|
|
67742
|
-
clearPendingById(item) {
|
|
67743
|
-
this.deps.queue.
|
|
67744
|
-
this.queuedItems.delete(item.id);
|
|
68035
|
+
clearPendingById(target, item) {
|
|
68036
|
+
this.deps.queue.clearPendingByIdsForKey(target.sessionKey, [item.id]);
|
|
68037
|
+
this.queuedItems.delete(this.buildManagedQueueKey(target.sessionKey, item.id));
|
|
67745
68038
|
}
|
|
67746
68039
|
async markQueueItemRunning(target, item) {
|
|
67747
68040
|
if (!item) {
|
|
@@ -67754,17 +68047,18 @@ class ManagedQueueController {
|
|
|
67754
68047
|
startedAt: now,
|
|
67755
68048
|
updatedAt: now
|
|
67756
68049
|
};
|
|
67757
|
-
this.queuedItems.set(item.id, {
|
|
68050
|
+
this.queuedItems.set(this.buildManagedQueueKey(target.sessionKey, item.id), {
|
|
67758
68051
|
target,
|
|
67759
68052
|
item: next
|
|
67760
68053
|
});
|
|
67761
68054
|
await this.deps.sessionState.replaceQueuedItemIfPresent(this.deps.resolveTarget(target), next);
|
|
68055
|
+
return next;
|
|
67762
68056
|
}
|
|
67763
68057
|
async removeManagedQueueItem(target, item) {
|
|
67764
68058
|
if (!item) {
|
|
67765
68059
|
return;
|
|
67766
68060
|
}
|
|
67767
|
-
this.queuedItems.delete(item.id);
|
|
68061
|
+
this.queuedItems.delete(this.buildManagedQueueKey(target.sessionKey, item.id));
|
|
67768
68062
|
await this.deps.sessionState.removeQueuedItem(this.deps.resolveTarget(target), item.id);
|
|
67769
68063
|
}
|
|
67770
68064
|
enqueuePersistedQueueItem(item) {
|
|
@@ -67772,28 +68066,43 @@ class ManagedQueueController {
|
|
|
67772
68066
|
agentId: item.agentId,
|
|
67773
68067
|
sessionKey: item.sessionKey
|
|
67774
68068
|
};
|
|
67775
|
-
this.queuedItems.set(item.id, {
|
|
68069
|
+
this.queuedItems.set(this.buildManagedQueueKey(target.sessionKey, item.id), {
|
|
67776
68070
|
target,
|
|
67777
68071
|
item
|
|
67778
68072
|
});
|
|
67779
68073
|
const queued = this.deps.queue.enqueue(item.sessionKey, async () => {
|
|
67780
|
-
await this.markQueueItemRunning(target, item);
|
|
68074
|
+
const runningItem = await this.markQueueItemRunning(target, item);
|
|
67781
68075
|
await this.deps.surfaceRuntime.notifyManagedQueueStart(target, item);
|
|
67782
68076
|
const promptText = await this.deps.surfaceRuntime.buildManagedQueuePrompt(item.agentId, item);
|
|
67783
|
-
|
|
68077
|
+
const result = this.deps.activeRuns.executePrompt(target, promptText, {
|
|
67784
68078
|
id: `queue:${item.id}`,
|
|
67785
68079
|
mode: "live",
|
|
67786
68080
|
onUpdate: async () => {
|
|
67787
68081
|
return;
|
|
67788
68082
|
}
|
|
67789
68083
|
});
|
|
68084
|
+
let stopWaitingForToolFinal = false;
|
|
68085
|
+
try {
|
|
68086
|
+
const outcome = await Promise.race([
|
|
68087
|
+
result.then((update) => ({ kind: "result", update })),
|
|
68088
|
+
this.waitForMessageToolFinal(target, runningItem ?? item, () => stopWaitingForToolFinal).then((seen) => ({ kind: seen ? "message-tool-final" : "result" }))
|
|
68089
|
+
]);
|
|
68090
|
+
if (outcome.kind === "message-tool-final") {
|
|
68091
|
+
return this.createMessageToolFinalQueueUpdate(target, item);
|
|
68092
|
+
}
|
|
68093
|
+
return await result;
|
|
68094
|
+
} finally {
|
|
68095
|
+
stopWaitingForToolFinal = true;
|
|
68096
|
+
}
|
|
67790
68097
|
}, {
|
|
67791
68098
|
id: item.id,
|
|
67792
68099
|
createdAt: item.createdAt,
|
|
67793
68100
|
text: item.promptSummary,
|
|
67794
|
-
canStart: async () =>
|
|
68101
|
+
canStart: async () => this.canStartPersistedQueueItem(target, item),
|
|
67795
68102
|
onComplete: async (value) => {
|
|
67796
|
-
await this.
|
|
68103
|
+
if (!value.messageToolFinalAlreadySent && !await this.hasMessageToolFinalForQueueItem(target, item)) {
|
|
68104
|
+
await this.deps.surfaceRuntime.notifyManagedQueueSettlement(target, item, value);
|
|
68105
|
+
}
|
|
67797
68106
|
await this.removeManagedQueueItem(target, item);
|
|
67798
68107
|
},
|
|
67799
68108
|
onFailure: async (error) => {
|
|
@@ -67803,38 +68112,54 @@ class ManagedQueueController {
|
|
|
67803
68112
|
onClear: () => this.removeManagedQueueItem(target, item)
|
|
67804
68113
|
});
|
|
67805
68114
|
queued.result.catch((error) => {
|
|
68115
|
+
if (error instanceof ClearedQueuedTaskError) {
|
|
68116
|
+
return;
|
|
68117
|
+
}
|
|
67806
68118
|
if (this.deps.shouldSuppressShutdownError(error)) {
|
|
67807
68119
|
return;
|
|
67808
68120
|
}
|
|
67809
68121
|
console.error("queued prompt execution failed", error);
|
|
67810
68122
|
});
|
|
67811
68123
|
}
|
|
67812
|
-
|
|
67813
|
-
|
|
67814
|
-
|
|
67815
|
-
|
|
67816
|
-
|
|
67817
|
-
|
|
67818
|
-
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
67819
|
-
function createSessionId() {
|
|
67820
|
-
return randomUUID4();
|
|
67821
|
-
}
|
|
67822
|
-
function extractSessionId(snapshot, pattern) {
|
|
67823
|
-
const regex = new RegExp(pattern, "ig");
|
|
67824
|
-
let lastMatch = null;
|
|
67825
|
-
for (;; ) {
|
|
67826
|
-
const match = regex.exec(snapshot);
|
|
67827
|
-
if (!match) {
|
|
67828
|
-
break;
|
|
68124
|
+
async waitForMessageToolFinal(target, item, shouldStop) {
|
|
68125
|
+
while (!shouldStop()) {
|
|
68126
|
+
if (await this.hasMessageToolFinalForQueueItem(target, item)) {
|
|
68127
|
+
return true;
|
|
68128
|
+
}
|
|
68129
|
+
await sleep(QUEUE_MESSAGE_TOOL_FINAL_POLL_MS);
|
|
67829
68130
|
}
|
|
67830
|
-
|
|
67831
|
-
|
|
68131
|
+
return false;
|
|
68132
|
+
}
|
|
68133
|
+
async hasMessageToolFinalForQueueItem(target, item) {
|
|
68134
|
+
const startedAt = this.queuedItems.get(this.buildManagedQueueKey(target.sessionKey, item.id))?.item.startedAt ?? item.startedAt;
|
|
68135
|
+
if (typeof startedAt !== "number" || !Number.isFinite(startedAt)) {
|
|
68136
|
+
return false;
|
|
67832
68137
|
}
|
|
67833
|
-
|
|
68138
|
+
const runtime = await this.deps.sessionState.getSessionRuntime(target);
|
|
68139
|
+
return typeof runtime.messageToolFinalReplyAt === "number" && Number.isFinite(runtime.messageToolFinalReplyAt) && runtime.messageToolFinalReplyAt >= startedAt;
|
|
68140
|
+
}
|
|
68141
|
+
createMessageToolFinalQueueUpdate(target, item) {
|
|
68142
|
+
const resolved = this.deps.resolveTarget(target);
|
|
68143
|
+
return {
|
|
68144
|
+
status: "completed",
|
|
68145
|
+
agentId: target.agentId,
|
|
68146
|
+
sessionKey: target.sessionKey,
|
|
68147
|
+
sessionName: resolved.sessionName,
|
|
68148
|
+
workspacePath: resolved.workspacePath,
|
|
68149
|
+
snapshot: "",
|
|
68150
|
+
fullSnapshot: "",
|
|
68151
|
+
initialSnapshot: "",
|
|
68152
|
+
messageToolFinalAlreadySent: true
|
|
68153
|
+
};
|
|
68154
|
+
}
|
|
68155
|
+
buildManagedQueueKey(sessionKey, itemId) {
|
|
68156
|
+
return `${sessionKey}::${itemId}`;
|
|
67834
68157
|
}
|
|
67835
|
-
return (lastMatch?.[1] ?? lastMatch?.[0] ?? "").trim() || null;
|
|
67836
68158
|
}
|
|
67837
68159
|
|
|
68160
|
+
// src/agents/runner-service.ts
|
|
68161
|
+
import { dirname as dirname12 } from "node:path";
|
|
68162
|
+
|
|
67838
68163
|
// src/shared/transcript-normalization.ts
|
|
67839
68164
|
import { stripVTControlCharacters } from "node:util";
|
|
67840
68165
|
function normalizePaneText(raw) {
|
|
@@ -68089,18 +68414,37 @@ function isTimerDrivenStatusLine(line) {
|
|
|
68089
68414
|
return isActiveTimerStatusLine(trimmed) || CLAUDE_WORKED_STATUS_PATTERN.test(trimmed);
|
|
68090
68415
|
}
|
|
68091
68416
|
function hasActiveTimerStatus(snapshot) {
|
|
68092
|
-
return
|
|
68417
|
+
return Boolean(extractLatestActiveTimerStatusLine(snapshot));
|
|
68093
68418
|
}
|
|
68094
68419
|
function extractLatestActiveTimerStatusLine(snapshot) {
|
|
68095
68420
|
const lines = splitNormalizedLines(snapshot);
|
|
68096
68421
|
for (let index = lines.length - 1;index >= 0; index -= 1) {
|
|
68097
68422
|
const line = lines[index]?.trim() ?? "";
|
|
68098
|
-
if (isActiveTimerStatusLine(line)) {
|
|
68423
|
+
if (isActiveTimerStatusLine(line) && isLiveTimerStatusLine(lines, index)) {
|
|
68099
68424
|
return line;
|
|
68100
68425
|
}
|
|
68101
68426
|
}
|
|
68102
68427
|
return "";
|
|
68103
68428
|
}
|
|
68429
|
+
function isLiveTimerStatusLine(lines, timerIndex) {
|
|
68430
|
+
for (let index = timerIndex + 1;index < lines.length; index += 1) {
|
|
68431
|
+
const trimmed = lines[index]?.trim() ?? "";
|
|
68432
|
+
if (!trimmed) {
|
|
68433
|
+
continue;
|
|
68434
|
+
}
|
|
68435
|
+
if (isRunnerIdlePromptLine(trimmed) || isRunnerPromptMetadataLine(trimmed)) {
|
|
68436
|
+
continue;
|
|
68437
|
+
}
|
|
68438
|
+
return false;
|
|
68439
|
+
}
|
|
68440
|
+
return true;
|
|
68441
|
+
}
|
|
68442
|
+
function isRunnerIdlePromptLine(trimmed) {
|
|
68443
|
+
return trimmed.startsWith("› ") || trimmed === "›" || trimmed.startsWith("> ");
|
|
68444
|
+
}
|
|
68445
|
+
function isRunnerPromptMetadataLine(trimmed) {
|
|
68446
|
+
return /^gpt-[\w.-]+ .*·/.test(trimmed) || trimmed.startsWith("model:") || trimmed.startsWith("directory:") || trimmed.startsWith("ctx:") || trimmed.startsWith("tokens:") || trimmed.startsWith("? for shortcuts");
|
|
68447
|
+
}
|
|
68104
68448
|
function shouldDropCodexChromeLine(line) {
|
|
68105
68449
|
const trimmed = line.trim();
|
|
68106
68450
|
if (!trimmed) {
|
|
@@ -68317,6 +68661,11 @@ function deriveInteractionText(initialSnapshot, currentSnapshot) {
|
|
|
68317
68661
|
const current = cleanInteractionSnapshot(currentSnapshot);
|
|
68318
68662
|
return extractScrolledAppend(previous, current) || diffText(previous, current);
|
|
68319
68663
|
}
|
|
68664
|
+
function deriveInteractionDiffText(initialSnapshot, currentSnapshot) {
|
|
68665
|
+
const previous = cleanInteractionSnapshot(initialSnapshot);
|
|
68666
|
+
const current = cleanInteractionSnapshot(currentSnapshot);
|
|
68667
|
+
return diffText(previous, current);
|
|
68668
|
+
}
|
|
68320
68669
|
function appendInteractionText(currentBody, nextDelta) {
|
|
68321
68670
|
const trimmedCurrent = currentBody.trim();
|
|
68322
68671
|
const trimmedDelta = nextDelta.trim();
|
|
@@ -68773,6 +69122,14 @@ class TmuxSubmitUnconfirmedError extends Error {
|
|
|
68773
69122
|
}
|
|
68774
69123
|
}
|
|
68775
69124
|
async function submitTmuxSessionInput(params) {
|
|
69125
|
+
if (params.trustPrompt) {
|
|
69126
|
+
await acceptTmuxTrustPromptIfPresent({
|
|
69127
|
+
tmux: params.tmux,
|
|
69128
|
+
sessionName: params.sessionName,
|
|
69129
|
+
captureLines: params.trustPrompt.captureLines,
|
|
69130
|
+
startupDelayMs: params.trustPrompt.startupDelayMs
|
|
69131
|
+
});
|
|
69132
|
+
}
|
|
68776
69133
|
const prePasteState = await params.tmux.getPaneState(params.sessionName);
|
|
68777
69134
|
const captureLines = estimatePasteCaptureLines(params.text);
|
|
68778
69135
|
const prePasteSnapshot = normalizePaneText(await params.tmux.capturePane(params.sessionName, captureLines));
|
|
@@ -68826,6 +69183,12 @@ async function submitTmuxSessionInput(params) {
|
|
|
68826
69183
|
throw new TmuxSubmitUnconfirmedError;
|
|
68827
69184
|
}
|
|
68828
69185
|
async function captureTmuxSessionIdentity(params) {
|
|
69186
|
+
await acceptTmuxTrustPromptIfPresent({
|
|
69187
|
+
tmux: params.tmux,
|
|
69188
|
+
sessionName: params.sessionName,
|
|
69189
|
+
captureLines: params.captureLines,
|
|
69190
|
+
startupDelayMs: params.timeoutMs
|
|
69191
|
+
});
|
|
68829
69192
|
let statusSubmission = await submitTmuxSessionInput({
|
|
68830
69193
|
tmux: params.tmux,
|
|
68831
69194
|
sessionName: params.sessionName,
|
|
@@ -68849,7 +69212,7 @@ async function captureTmuxSessionIdentity(params) {
|
|
|
68849
69212
|
throw error;
|
|
68850
69213
|
}
|
|
68851
69214
|
if (tmuxPaneHasTrustPrompt(snapshot)) {
|
|
68852
|
-
await
|
|
69215
|
+
await acceptTrustPrompt({
|
|
68853
69216
|
tmux: params.tmux,
|
|
68854
69217
|
sessionName: params.sessionName,
|
|
68855
69218
|
captureLines: params.captureLines
|
|
@@ -68864,7 +69227,7 @@ async function captureTmuxSessionIdentity(params) {
|
|
|
68864
69227
|
});
|
|
68865
69228
|
continue;
|
|
68866
69229
|
}
|
|
68867
|
-
const sessionId =
|
|
69230
|
+
const sessionId = extractSessionIdFromCaptureCandidates(deriveSessionIdCaptureCandidates(statusSubmission.submittedSnapshot, snapshot, params.statusCommand), params.pattern);
|
|
68868
69231
|
if (sessionId) {
|
|
68869
69232
|
await waitForTmuxPaneSettle({
|
|
68870
69233
|
tmux: params.tmux,
|
|
@@ -68879,12 +69242,34 @@ async function captureTmuxSessionIdentity(params) {
|
|
|
68879
69242
|
}
|
|
68880
69243
|
return null;
|
|
68881
69244
|
}
|
|
68882
|
-
function
|
|
69245
|
+
function deriveSessionIdCaptureCandidates(submittedSnapshot, snapshot, statusCommand) {
|
|
68883
69246
|
const rawSubmitted = normalizePaneText(submittedSnapshot);
|
|
68884
69247
|
const rawSnapshot = normalizePaneText(snapshot);
|
|
68885
|
-
return
|
|
69248
|
+
return [
|
|
69249
|
+
extractScrolledAppend(rawSubmitted, rawSnapshot),
|
|
69250
|
+
deriveInteractionText(submittedSnapshot, snapshot),
|
|
69251
|
+
deriveInteractionDiffText(submittedSnapshot, snapshot),
|
|
69252
|
+
rawSnapshot,
|
|
69253
|
+
extractStatusCommandTail(rawSnapshot, statusCommand)
|
|
69254
|
+
].filter((candidate, index, candidates) => candidate && candidates.indexOf(candidate) === index);
|
|
69255
|
+
}
|
|
69256
|
+
function extractStatusCommandTail(snapshot, statusCommand) {
|
|
69257
|
+
const lastStatusIndex = snapshot.lastIndexOf(statusCommand);
|
|
69258
|
+
if (lastStatusIndex < 0) {
|
|
69259
|
+
return "";
|
|
69260
|
+
}
|
|
69261
|
+
return snapshot.slice(lastStatusIndex);
|
|
69262
|
+
}
|
|
69263
|
+
function extractSessionIdFromCaptureCandidates(candidates, pattern) {
|
|
69264
|
+
for (const candidate of candidates) {
|
|
69265
|
+
const sessionId = parseRunnerSessionId(candidate, pattern);
|
|
69266
|
+
if (sessionId) {
|
|
69267
|
+
return sessionId;
|
|
69268
|
+
}
|
|
69269
|
+
}
|
|
69270
|
+
return null;
|
|
68886
69271
|
}
|
|
68887
|
-
async function
|
|
69272
|
+
async function acceptTmuxTrustPromptIfPresent(params) {
|
|
68888
69273
|
const deadline = Date.now() + Math.max(TRUST_PROMPT_MAX_WAIT_MS, params.startupDelayMs);
|
|
68889
69274
|
while (Date.now() <= deadline) {
|
|
68890
69275
|
let snapshot = "";
|
|
@@ -68907,7 +69292,7 @@ async function dismissTmuxTrustPromptIfPresent(params) {
|
|
|
68907
69292
|
if (!tmuxPaneHasTrustPrompt(snapshot)) {
|
|
68908
69293
|
return;
|
|
68909
69294
|
}
|
|
68910
|
-
await
|
|
69295
|
+
await acceptTrustPrompt({
|
|
68911
69296
|
tmux: params.tmux,
|
|
68912
69297
|
sessionName: params.sessionName,
|
|
68913
69298
|
captureLines: params.captureLines
|
|
@@ -68939,7 +69324,7 @@ async function waitForTmuxSessionBootstrap(params) {
|
|
|
68939
69324
|
if (snapshot) {
|
|
68940
69325
|
lastSnapshot = snapshot;
|
|
68941
69326
|
if (params.trustWorkspace && tmuxPaneHasTrustPrompt(snapshot)) {
|
|
68942
|
-
await
|
|
69327
|
+
await acceptTrustPrompt({
|
|
68943
69328
|
tmux: params.tmux,
|
|
68944
69329
|
sessionName: params.sessionName,
|
|
68945
69330
|
captureLines: params.captureLines
|
|
@@ -68956,7 +69341,7 @@ async function waitForTmuxSessionBootstrap(params) {
|
|
|
68956
69341
|
};
|
|
68957
69342
|
}
|
|
68958
69343
|
}
|
|
68959
|
-
if (readyRegex && !
|
|
69344
|
+
if (readyRegex && !snapshotHasActiveReadyPattern(snapshot, readyRegex)) {
|
|
68960
69345
|
await sleep(SESSION_BOOTSTRAP_POLL_INTERVAL_MS);
|
|
68961
69346
|
continue;
|
|
68962
69347
|
}
|
|
@@ -68972,7 +69357,7 @@ async function waitForTmuxSessionBootstrap(params) {
|
|
|
68972
69357
|
snapshot: lastSnapshot
|
|
68973
69358
|
};
|
|
68974
69359
|
}
|
|
68975
|
-
async function
|
|
69360
|
+
async function acceptTrustPrompt(params) {
|
|
68976
69361
|
await params.tmux.sendKey(params.sessionName, "Enter");
|
|
68977
69362
|
const deadline = Date.now() + TRUST_PROMPT_MAX_WAIT_MS;
|
|
68978
69363
|
while (Date.now() <= deadline) {
|
|
@@ -69190,6 +69575,29 @@ function buildBootstrapSessionLostError(sessionName, error) {
|
|
|
69190
69575
|
function arePaneStatesEqual(left, right) {
|
|
69191
69576
|
return left.cursorX === right.cursorX && left.cursorY === right.cursorY && left.historySize === right.historySize;
|
|
69192
69577
|
}
|
|
69578
|
+
function snapshotHasActiveReadyPattern(snapshot, readyRegex) {
|
|
69579
|
+
const lines = splitNormalizedLines(snapshot);
|
|
69580
|
+
let readyLineIndex = -1;
|
|
69581
|
+
for (let index = 0;index < lines.length; index += 1) {
|
|
69582
|
+
if (readyRegex.test(lines[index] ?? "")) {
|
|
69583
|
+
readyLineIndex = index;
|
|
69584
|
+
}
|
|
69585
|
+
}
|
|
69586
|
+
if (readyLineIndex < 0) {
|
|
69587
|
+
return readyRegex.test(snapshot);
|
|
69588
|
+
}
|
|
69589
|
+
for (const rawLine of lines.slice(readyLineIndex + 1)) {
|
|
69590
|
+
const line = rawLine.trim();
|
|
69591
|
+
if (!line || isPromptMetadataLine(line)) {
|
|
69592
|
+
continue;
|
|
69593
|
+
}
|
|
69594
|
+
return false;
|
|
69595
|
+
}
|
|
69596
|
+
return true;
|
|
69597
|
+
}
|
|
69598
|
+
function isPromptMetadataLine(line) {
|
|
69599
|
+
return /^gpt-[\w.-]+\b/i.test(line) || /^model:\s*/i.test(line) || /^session:\s*/i.test(line);
|
|
69600
|
+
}
|
|
69193
69601
|
function looksLikeClaudeTrustPrompt(snapshot) {
|
|
69194
69602
|
return snapshot.includes("Quick safety check:") && snapshot.includes("Yes, I trust this folder") || snapshot.includes("Enter to confirm · Esc to cancel");
|
|
69195
69603
|
}
|
|
@@ -69364,8 +69772,10 @@ var TMUX_DUPLICATE_SESSION_PATTERN = /duplicate session:/i;
|
|
|
69364
69772
|
var TMUX_TRANSIENT_TARGET_PATTERN = /(?:no current target|can't find pane|can't find window|no such pane|no such window|tmux pane state unavailable)/i;
|
|
69365
69773
|
var SESSION_READY_CAPTURE_RETRY_COUNT = 5;
|
|
69366
69774
|
var SESSION_READY_CAPTURE_RETRY_DELAY_MS = 100;
|
|
69367
|
-
var
|
|
69775
|
+
var STARTUP_SESSION_ID_CAPTURE_RETRY_COUNT = 2;
|
|
69776
|
+
var STARTUP_SESSION_ID_CAPTURE_RETRY_DELAY_MS = 500;
|
|
69368
69777
|
var SESSION_ID_CAPTURE_FAILURE_COOLDOWN_MS = 15000;
|
|
69778
|
+
var PRESERVED_SESSION_ID_RETRY_MESSAGE = "The previous runner session could not be resumed. clisbot preserved the stored session id instead of opening a new conversation automatically. Use `/new` if you want to trigger a new runner conversation, then resend the prompt.";
|
|
69369
69779
|
function summarizeSnapshot(snapshot) {
|
|
69370
69780
|
const compact = snapshot.split(`
|
|
69371
69781
|
`).map((line) => line.trim()).filter(Boolean).join(" ").slice(0, 220);
|
|
@@ -69396,19 +69806,24 @@ function isFreshStartRetryablePromptDeliveryError(error) {
|
|
|
69396
69806
|
function isRetryableFreshStartFault(error) {
|
|
69397
69807
|
return isRecoverableStartupSessionLoss(error) || isTransientTmuxTargetError(error) || isFreshStartRetryablePromptDeliveryError(error);
|
|
69398
69808
|
}
|
|
69809
|
+
function canRestartWithStoredSessionId(resolved) {
|
|
69810
|
+
return resolved.runner.sessionId.resume.mode === "command" || resolved.runner.sessionId.create.mode === "explicit";
|
|
69811
|
+
}
|
|
69399
69812
|
|
|
69400
69813
|
class RunnerService {
|
|
69401
69814
|
loadedConfig;
|
|
69402
69815
|
tmux;
|
|
69403
69816
|
sessionState;
|
|
69404
69817
|
resolveTarget;
|
|
69818
|
+
sessionMapping;
|
|
69405
69819
|
cleanupInFlight = false;
|
|
69406
69820
|
sessionIdentityCaptureRetryAt = new Map;
|
|
69407
|
-
constructor(loadedConfig, tmux, sessionState, resolveTarget) {
|
|
69821
|
+
constructor(loadedConfig, tmux, sessionState, resolveTarget, sessionMapping = new SessionMapping(sessionState)) {
|
|
69408
69822
|
this.loadedConfig = loadedConfig;
|
|
69409
69823
|
this.tmux = tmux;
|
|
69410
69824
|
this.sessionState = sessionState;
|
|
69411
69825
|
this.resolveTarget = resolveTarget;
|
|
69826
|
+
this.sessionMapping = sessionMapping;
|
|
69412
69827
|
}
|
|
69413
69828
|
async mapSessionError(error, sessionName, action, lastSnapshot = "") {
|
|
69414
69829
|
if (isRecoverableStartupSessionLoss(error)) {
|
|
@@ -69452,24 +69867,21 @@ class RunnerService {
|
|
|
69452
69867
|
args: args.map((value) => applyTemplate(value, values))
|
|
69453
69868
|
};
|
|
69454
69869
|
}
|
|
69455
|
-
async
|
|
69456
|
-
const existing = await this.
|
|
69870
|
+
async syncStoredSessionIdForResolvedTarget(resolved) {
|
|
69871
|
+
const existing = await this.sessionMapping.get(resolved.sessionKey);
|
|
69457
69872
|
if (existing?.sessionId) {
|
|
69458
|
-
this.
|
|
69459
|
-
return
|
|
69460
|
-
sessionId: existing.sessionId,
|
|
69461
|
-
runnerCommand: resolved.runner.command
|
|
69462
|
-
});
|
|
69873
|
+
await this.persistStoredSessionIdBestEffort(resolved, existing.sessionId, resolved.runner.command);
|
|
69874
|
+
return existing;
|
|
69463
69875
|
}
|
|
69464
69876
|
const retryAt = this.sessionIdentityCaptureRetryAt.get(resolved.sessionKey) ?? 0;
|
|
69465
69877
|
if (retryAt > Date.now()) {
|
|
69466
|
-
return this.
|
|
69878
|
+
return this.sessionMapping.touch(resolved, {
|
|
69467
69879
|
runnerCommand: resolved.runner.command
|
|
69468
69880
|
});
|
|
69469
69881
|
}
|
|
69470
69882
|
let sessionId;
|
|
69471
69883
|
try {
|
|
69472
|
-
sessionId = await this.
|
|
69884
|
+
sessionId = await this.captureSessionIdFromRunner(resolved);
|
|
69473
69885
|
} catch (error) {
|
|
69474
69886
|
if (isFreshStartRetryablePromptDeliveryError(error)) {
|
|
69475
69887
|
this.sessionIdentityCaptureRetryAt.set(resolved.sessionKey, Date.now() + SESSION_ID_CAPTURE_FAILURE_COOLDOWN_MS);
|
|
@@ -69477,16 +69889,36 @@ class RunnerService {
|
|
|
69477
69889
|
throw error;
|
|
69478
69890
|
}
|
|
69479
69891
|
if (sessionId) {
|
|
69480
|
-
this.
|
|
69481
|
-
|
|
69482
|
-
|
|
69892
|
+
await this.persistStoredSessionIdBestEffort(resolved, sessionId, resolved.runner.command);
|
|
69893
|
+
return {
|
|
69894
|
+
sessionId
|
|
69895
|
+
};
|
|
69483
69896
|
}
|
|
69484
|
-
|
|
69485
|
-
|
|
69897
|
+
this.deferStoredSessionIdCapture(resolved.sessionKey);
|
|
69898
|
+
return this.sessionMapping.touch(resolved, {
|
|
69486
69899
|
runnerCommand: resolved.runner.command
|
|
69487
69900
|
});
|
|
69488
69901
|
}
|
|
69489
|
-
|
|
69902
|
+
persistStoredSessionId(resolved, sessionId, runnerCommand = resolved.runner.command) {
|
|
69903
|
+
this.sessionIdentityCaptureRetryAt.delete(resolved.sessionKey);
|
|
69904
|
+
return this.sessionMapping.setActive(resolved, {
|
|
69905
|
+
sessionId,
|
|
69906
|
+
runnerCommand
|
|
69907
|
+
});
|
|
69908
|
+
}
|
|
69909
|
+
async persistStoredSessionIdBestEffort(resolved, sessionId, runnerCommand = resolved.runner.command) {
|
|
69910
|
+
try {
|
|
69911
|
+
await this.persistStoredSessionId(resolved, sessionId, runnerCommand);
|
|
69912
|
+
return true;
|
|
69913
|
+
} catch (error) {
|
|
69914
|
+
this.warnStartupSessionIdentityDegraded(resolved, error);
|
|
69915
|
+
return false;
|
|
69916
|
+
}
|
|
69917
|
+
}
|
|
69918
|
+
deferStoredSessionIdCapture(sessionKey) {
|
|
69919
|
+
this.sessionIdentityCaptureRetryAt.set(sessionKey, Date.now() + SESSION_ID_CAPTURE_FAILURE_COOLDOWN_MS);
|
|
69920
|
+
}
|
|
69921
|
+
async captureSessionIdFromRunner(resolved, options = {}) {
|
|
69490
69922
|
const capture = resolved.runner.sessionId.capture;
|
|
69491
69923
|
if (capture.mode !== "status-command" && !options.forceStatusCommand) {
|
|
69492
69924
|
return null;
|
|
@@ -69514,12 +69946,39 @@ class RunnerService {
|
|
|
69514
69946
|
remainingFreshRetries: remainingFreshRetries - 1
|
|
69515
69947
|
});
|
|
69516
69948
|
}
|
|
69517
|
-
async retryAfterStartupFault(target, resolved, error, remainingFreshRetries) {
|
|
69949
|
+
async retryAfterStartupFault(target, resolved, error, remainingFreshRetries, allowFreshResumeFallback) {
|
|
69950
|
+
if (allowFreshResumeFallback) {
|
|
69951
|
+
const resumedFresh = await this.retryFreshStartAfterStoredResumeFailure(target, resolved, error, remainingFreshRetries);
|
|
69952
|
+
if (resumedFresh) {
|
|
69953
|
+
return resumedFresh;
|
|
69954
|
+
}
|
|
69955
|
+
}
|
|
69518
69956
|
if (!isRetryableFreshStartFault(error)) {
|
|
69519
69957
|
return null;
|
|
69520
69958
|
}
|
|
69521
69959
|
return this.retryRunnerRestartPreservingSessionId(target, resolved, remainingFreshRetries);
|
|
69522
69960
|
}
|
|
69961
|
+
async retryFreshStartAfterStoredResumeFailure(target, resolved, error, remainingFreshRetries) {
|
|
69962
|
+
if (!isRecoverableStartupSessionLoss(error)) {
|
|
69963
|
+
return null;
|
|
69964
|
+
}
|
|
69965
|
+
if (resolved.runner.sessionId.resume.mode !== "command" || resolved.runner.sessionId.create.mode !== "runner") {
|
|
69966
|
+
return null;
|
|
69967
|
+
}
|
|
69968
|
+
const existing = await this.sessionMapping.get(resolved.sessionKey);
|
|
69969
|
+
if (!existing?.sessionId) {
|
|
69970
|
+
return null;
|
|
69971
|
+
}
|
|
69972
|
+
const exitRecord = await readRunnerExitRecord(this.loadedConfig.stateDir, resolved.sessionName);
|
|
69973
|
+
if (!exitRecord || exitRecord.exitCode === 0) {
|
|
69974
|
+
return null;
|
|
69975
|
+
}
|
|
69976
|
+
console.log(`clisbot preserved stored sessionId after failed runner resume startup ${resolved.sessionName}`);
|
|
69977
|
+
await this.sessionMapping.touch(resolved, {
|
|
69978
|
+
runnerCommand: resolved.runner.command
|
|
69979
|
+
});
|
|
69980
|
+
throw new Error(PRESERVED_SESSION_ID_RETRY_MESSAGE);
|
|
69981
|
+
}
|
|
69523
69982
|
async retryAfterStartupTimeout(target, resolved, remainingFreshRetries) {
|
|
69524
69983
|
return this.retryRunnerRestartPreservingSessionId(target, resolved, remainingFreshRetries);
|
|
69525
69984
|
}
|
|
@@ -69547,7 +70006,7 @@ class RunnerService {
|
|
|
69547
70006
|
try {
|
|
69548
70007
|
const snapshot = await this.captureSessionSnapshot(resolved);
|
|
69549
70008
|
if (tmuxPaneHasTrustPrompt(snapshot)) {
|
|
69550
|
-
await this.
|
|
70009
|
+
await this.acceptVisibleWorkspaceTrustPrompt(resolved);
|
|
69551
70010
|
continue;
|
|
69552
70011
|
}
|
|
69553
70012
|
return;
|
|
@@ -69610,18 +70069,16 @@ class RunnerService {
|
|
|
69610
70069
|
await ensureDir2(resolved.workspacePath);
|
|
69611
70070
|
await ensureDir2(dirname12(this.loadedConfig.raw.tmux.socketPath));
|
|
69612
70071
|
await ensureRunnerExitRecordDir(this.loadedConfig.stateDir, resolved.sessionName);
|
|
69613
|
-
const
|
|
70072
|
+
const preparedMapping = await this.sessionMapping.prepareStartup(resolved);
|
|
69614
70073
|
const serverRunning = await this.tmux.isServerRunning();
|
|
69615
70074
|
if (serverRunning && await this.tmux.hasSession(resolved.sessionName)) {
|
|
69616
70075
|
logLatencyDebug("ensure-session-ready-existing-session", timingContext, {
|
|
69617
|
-
hasStoredSessionId: Boolean(
|
|
70076
|
+
hasStoredSessionId: Boolean(preparedMapping.storedSessionId)
|
|
69618
70077
|
});
|
|
69619
70078
|
try {
|
|
69620
70079
|
await clearRunnerExitRecord(this.loadedConfig.stateDir, resolved.sessionName);
|
|
69621
|
-
await this.
|
|
69622
|
-
|
|
69623
|
-
runnerCommand: resolved.runner.command
|
|
69624
|
-
});
|
|
70080
|
+
await this.acceptWorkspaceTrustPromptIfPresent(resolved);
|
|
70081
|
+
await this.syncStoredSessionIdForResolvedTarget(resolved);
|
|
69625
70082
|
} catch (error) {
|
|
69626
70083
|
throw await this.mapSessionError(error, resolved.sessionName, "during startup");
|
|
69627
70084
|
}
|
|
@@ -69634,10 +70091,10 @@ class RunnerService {
|
|
|
69634
70091
|
if (!resolved.session.createIfMissing) {
|
|
69635
70092
|
throw new Error(`tmux session "${resolved.sessionName}" does not exist`);
|
|
69636
70093
|
}
|
|
69637
|
-
const
|
|
69638
|
-
const resumingExistingSession =
|
|
70094
|
+
const storedOrExplicitSessionId = preparedMapping.sessionId ?? "";
|
|
70095
|
+
const resumingExistingSession = preparedMapping.resume;
|
|
69639
70096
|
const runnerLaunch = this.buildRunnerArgs(resolved, {
|
|
69640
|
-
sessionId:
|
|
70097
|
+
sessionId: storedOrExplicitSessionId || undefined,
|
|
69641
70098
|
resume: resumingExistingSession
|
|
69642
70099
|
});
|
|
69643
70100
|
await clearRunnerExitRecord(this.loadedConfig.stateDir, resolved.sessionName);
|
|
@@ -69665,7 +70122,7 @@ class RunnerService {
|
|
|
69665
70122
|
logLatencyDebug("ensure-session-ready-new-session", timingContext, {
|
|
69666
70123
|
startupDelayMs: resolved.runner.startupDelayMs,
|
|
69667
70124
|
resumingExistingSession,
|
|
69668
|
-
hasStoredSessionId: Boolean(
|
|
70125
|
+
hasStoredSessionId: Boolean(preparedMapping.storedSessionId)
|
|
69669
70126
|
});
|
|
69670
70127
|
const bootstrapResult = await waitForTmuxSessionBootstrap({
|
|
69671
70128
|
tmux: this.tmux,
|
|
@@ -69687,11 +70144,11 @@ class RunnerService {
|
|
|
69687
70144
|
await this.abortUnreadySession(resolved, `Runner session "${resolved.sessionName}" did not reach the configured ready state within ${resolved.runner.startupDelayMs}ms.`, bootstrapResult.snapshot);
|
|
69688
70145
|
}
|
|
69689
70146
|
await this.finalizeSessionStartup(resolved, {
|
|
69690
|
-
|
|
70147
|
+
storedOrExplicitSessionId,
|
|
69691
70148
|
runnerCommand: runnerLaunch.command
|
|
69692
70149
|
});
|
|
69693
70150
|
} catch (error) {
|
|
69694
|
-
const retried = await this.retryAfterStartupFault(target, resolved, error, remainingFreshRetries);
|
|
70151
|
+
const retried = await this.retryAfterStartupFault(target, resolved, error, remainingFreshRetries, options.allowFreshRetry !== false);
|
|
69695
70152
|
if (retried) {
|
|
69696
70153
|
return retried;
|
|
69697
70154
|
}
|
|
@@ -69704,25 +70161,48 @@ class RunnerService {
|
|
|
69704
70161
|
return resolved;
|
|
69705
70162
|
}
|
|
69706
70163
|
async finalizeSessionStartup(resolved, params) {
|
|
69707
|
-
await this.
|
|
70164
|
+
await this.acceptWorkspaceTrustPromptIfPresent(resolved);
|
|
69708
70165
|
await this.verifySessionReady(resolved);
|
|
69709
|
-
if (params.
|
|
69710
|
-
await this.
|
|
69711
|
-
sessionId: params.startupSessionId,
|
|
69712
|
-
runnerCommand: params.runnerCommand
|
|
69713
|
-
});
|
|
70166
|
+
if (params.storedOrExplicitSessionId) {
|
|
70167
|
+
await this.persistStoredSessionIdBestEffort(resolved, params.storedOrExplicitSessionId, params.runnerCommand);
|
|
69714
70168
|
return;
|
|
69715
70169
|
}
|
|
69716
|
-
await this.
|
|
70170
|
+
const entry = await this.syncStoredSessionIdForResolvedTarget(resolved);
|
|
70171
|
+
if (entry?.sessionId) {
|
|
70172
|
+
return;
|
|
70173
|
+
}
|
|
70174
|
+
await this.retryMissingStoredSessionIdAfterStartup(resolved);
|
|
69717
70175
|
}
|
|
69718
|
-
|
|
70176
|
+
warnStartupSessionIdentityDegraded(resolved, error) {
|
|
70177
|
+
console.warn(`clisbot could not persist or confirm a durable sessionId after startup for ${resolved.sessionName}; continuing without resumable state`, error);
|
|
70178
|
+
}
|
|
70179
|
+
async retryMissingStoredSessionIdAfterStartup(resolved) {
|
|
70180
|
+
for (let attempt = 0;attempt < STARTUP_SESSION_ID_CAPTURE_RETRY_COUNT; attempt += 1) {
|
|
70181
|
+
await sleep(STARTUP_SESSION_ID_CAPTURE_RETRY_DELAY_MS);
|
|
70182
|
+
let sessionId = null;
|
|
70183
|
+
try {
|
|
70184
|
+
sessionId = await this.captureSessionIdFromRunner(resolved);
|
|
70185
|
+
} catch (error) {
|
|
70186
|
+
if (isRecoverableStartupSessionLoss(error) || isTransientTmuxTargetError(error) || isFreshStartRetryablePromptDeliveryError(error)) {
|
|
70187
|
+
continue;
|
|
70188
|
+
}
|
|
70189
|
+
return;
|
|
70190
|
+
}
|
|
70191
|
+
if (!sessionId) {
|
|
70192
|
+
continue;
|
|
70193
|
+
}
|
|
70194
|
+
await this.persistStoredSessionIdBestEffort(resolved, sessionId);
|
|
70195
|
+
return;
|
|
70196
|
+
}
|
|
70197
|
+
}
|
|
70198
|
+
async acceptWorkspaceTrustPromptIfPresent(resolved) {
|
|
69719
70199
|
if (!resolved.runner.trustWorkspace) {
|
|
69720
70200
|
return;
|
|
69721
70201
|
}
|
|
69722
|
-
await this.
|
|
70202
|
+
await this.acceptVisibleWorkspaceTrustPrompt(resolved);
|
|
69723
70203
|
}
|
|
69724
|
-
async
|
|
69725
|
-
await
|
|
70204
|
+
async acceptVisibleWorkspaceTrustPrompt(resolved) {
|
|
70205
|
+
await acceptTmuxTrustPromptIfPresent({
|
|
69726
70206
|
tmux: this.tmux,
|
|
69727
70207
|
sessionName: resolved.sessionName,
|
|
69728
70208
|
captureLines: resolved.stream.captureLines,
|
|
@@ -69765,8 +70245,8 @@ class RunnerService {
|
|
|
69765
70245
|
}
|
|
69766
70246
|
async reopenRunContext(target, timingContext) {
|
|
69767
70247
|
const resolved = this.resolveTarget(target);
|
|
69768
|
-
const existing = await this.
|
|
69769
|
-
if (!existing?.sessionId || resolved
|
|
70248
|
+
const existing = await this.sessionMapping.get(resolved.sessionKey);
|
|
70249
|
+
if (!existing?.sessionId || !canRestartWithStoredSessionId(resolved)) {
|
|
69770
70250
|
throw new Error(`Runner session "${resolved.sessionName}" cannot reopen the same conversation context.`);
|
|
69771
70251
|
}
|
|
69772
70252
|
return this.ensureRunnerReady(target, { allowFreshRetryBeforePrompt: false, timingContext });
|
|
@@ -69777,7 +70257,9 @@ class RunnerService {
|
|
|
69777
70257
|
return;
|
|
69778
70258
|
});
|
|
69779
70259
|
console.log(`clisbot clearing stored sessionId for explicit fresh session ${resolved.sessionName}`);
|
|
69780
|
-
await this.
|
|
70260
|
+
await this.sessionMapping.clearActive(resolved, {
|
|
70261
|
+
runnerCommand: resolved.runner.command
|
|
70262
|
+
});
|
|
69781
70263
|
return this.ensureRunnerReady(target, {
|
|
69782
70264
|
allowFreshRetryBeforePrompt: false,
|
|
69783
70265
|
timingContext
|
|
@@ -69799,26 +70281,37 @@ class RunnerService {
|
|
|
69799
70281
|
});
|
|
69800
70282
|
}
|
|
69801
70283
|
async triggerNewSessionInLiveRunner(resolved) {
|
|
69802
|
-
const oldSessionId = (await this.
|
|
70284
|
+
const oldSessionId = (await this.sessionMapping.get(resolved.sessionKey))?.sessionId;
|
|
69803
70285
|
const command = this.resolveNewSessionCommand(resolved);
|
|
69804
|
-
|
|
69805
|
-
|
|
70286
|
+
await this.acceptWorkspaceTrustPromptIfPresent(resolved);
|
|
70287
|
+
let submitUnconfirmedError = null;
|
|
70288
|
+
try {
|
|
69806
70289
|
await this.submitNewSessionCommand(resolved, command);
|
|
69807
|
-
|
|
69808
|
-
if (
|
|
69809
|
-
|
|
70290
|
+
} catch (error) {
|
|
70291
|
+
if (error instanceof TmuxSubmitUnconfirmedError) {
|
|
70292
|
+
submitUnconfirmedError = error;
|
|
70293
|
+
} else {
|
|
70294
|
+
throw error;
|
|
69810
70295
|
}
|
|
69811
70296
|
}
|
|
70297
|
+
const sessionId = await this.captureNewSessionIdentityAfterTrigger(resolved, oldSessionId);
|
|
69812
70298
|
if (!sessionId) {
|
|
69813
|
-
|
|
69814
|
-
|
|
69815
|
-
await this.sessionState.touchSessionEntry(resolved, {
|
|
69816
|
-
sessionId,
|
|
69817
|
-
runnerCommand: resolved.runner.command,
|
|
69818
|
-
runtime: {
|
|
69819
|
-
state: "idle"
|
|
70299
|
+
if (submitUnconfirmedError) {
|
|
70300
|
+
throw submitUnconfirmedError;
|
|
69820
70301
|
}
|
|
69821
|
-
|
|
70302
|
+
this.throwNewSessionCaptureFailure(command, oldSessionId);
|
|
70303
|
+
}
|
|
70304
|
+
try {
|
|
70305
|
+
await this.sessionMapping.setActive(resolved, {
|
|
70306
|
+
sessionId,
|
|
70307
|
+
runnerCommand: resolved.runner.command,
|
|
70308
|
+
runtime: {
|
|
70309
|
+
state: "idle"
|
|
70310
|
+
}
|
|
70311
|
+
});
|
|
70312
|
+
} catch (error) {
|
|
70313
|
+
this.throwNewSessionPersistFailure(command, sessionId, error);
|
|
70314
|
+
}
|
|
69822
70315
|
return {
|
|
69823
70316
|
agentId: resolved.agentId,
|
|
69824
70317
|
sessionKey: resolved.sessionKey,
|
|
@@ -69831,7 +70324,7 @@ class RunnerService {
|
|
|
69831
70324
|
}
|
|
69832
70325
|
async restartRunnerWithFreshSessionIdForNewCommand(target) {
|
|
69833
70326
|
const { resolved } = await this.restartRunnerWithFreshSessionId(target);
|
|
69834
|
-
const entry = await this.
|
|
70327
|
+
const entry = await this.sessionMapping.get(resolved.sessionKey);
|
|
69835
70328
|
return {
|
|
69836
70329
|
agentId: resolved.agentId,
|
|
69837
70330
|
sessionKey: resolved.sessionKey,
|
|
@@ -69852,29 +70345,34 @@ class RunnerService {
|
|
|
69852
70345
|
});
|
|
69853
70346
|
}
|
|
69854
70347
|
async captureNewSessionIdentityAfterTrigger(resolved, oldSessionId) {
|
|
69855
|
-
for (let attempt = 0;attempt <
|
|
69856
|
-
const sessionId = await this.
|
|
70348
|
+
for (let attempt = 0;attempt < SESSION_READY_CAPTURE_RETRY_COUNT; attempt += 1) {
|
|
70349
|
+
const sessionId = await this.captureSessionIdFromRunner(resolved, {
|
|
69857
70350
|
forceStatusCommand: true
|
|
69858
70351
|
});
|
|
69859
70352
|
if (sessionId && sessionId !== oldSessionId) {
|
|
69860
70353
|
return sessionId;
|
|
69861
70354
|
}
|
|
69862
|
-
if (attempt <
|
|
70355
|
+
if (attempt < SESSION_READY_CAPTURE_RETRY_COUNT - 1) {
|
|
69863
70356
|
await sleep(SESSION_READY_CAPTURE_RETRY_DELAY_MS);
|
|
69864
70357
|
}
|
|
69865
70358
|
}
|
|
69866
70359
|
return null;
|
|
69867
70360
|
}
|
|
69868
|
-
|
|
69869
|
-
console.log(`clisbot
|
|
69870
|
-
|
|
69871
|
-
|
|
70361
|
+
throwNewSessionCaptureFailure(command, oldSessionId) {
|
|
70362
|
+
console.log(`clisbot preserved the previous stored sessionId after ${command} because status capture returned no id`);
|
|
70363
|
+
throw new Error(oldSessionId ? `${command} completed, but clisbot could not confirm the rotated session id. The previous stored session id was preserved instead of being cleared automatically.` : `${command} completed, but clisbot could not capture a new session id from the runner status command.`);
|
|
70364
|
+
}
|
|
70365
|
+
throwNewSessionPersistFailure(command, sessionId, error) {
|
|
70366
|
+
console.error(`clisbot failed to persist rotated sessionId after ${command}`, {
|
|
70367
|
+
sessionId,
|
|
70368
|
+
error
|
|
69872
70369
|
});
|
|
69873
|
-
|
|
70370
|
+
const details = error instanceof Error && error.message.trim() ? ` Persist error: ${error.message.trim()}` : "";
|
|
70371
|
+
throw new Error(`${command} completed and clisbot captured session id ${sessionId}, but could not persist it. The durable session mapping was left unchanged.${details}`);
|
|
69874
70372
|
}
|
|
69875
70373
|
async killRunnerAndPreserveSessionId(resolved) {
|
|
69876
70374
|
await this.tmux.killSession(resolved.sessionName);
|
|
69877
|
-
await this.
|
|
70375
|
+
await this.sessionMapping.touch(resolved, {
|
|
69878
70376
|
runnerCommand: resolved.runner.command
|
|
69879
70377
|
});
|
|
69880
70378
|
}
|
|
@@ -69892,7 +70390,6 @@ class RunnerService {
|
|
|
69892
70390
|
snapshot: ""
|
|
69893
70391
|
};
|
|
69894
70392
|
}
|
|
69895
|
-
await this.sessionState.touchSessionEntry(resolved);
|
|
69896
70393
|
try {
|
|
69897
70394
|
return {
|
|
69898
70395
|
agentId: resolved.agentId,
|
|
@@ -69918,14 +70415,13 @@ class RunnerService {
|
|
|
69918
70415
|
const resolved = this.resolveTarget(target);
|
|
69919
70416
|
const existed = await this.tmux.hasSession(resolved.sessionName);
|
|
69920
70417
|
if (existed) {
|
|
69921
|
-
await this.
|
|
70418
|
+
await this.sessionMapping.touch(resolved, {
|
|
69922
70419
|
runtime: {
|
|
69923
70420
|
state: "idle"
|
|
69924
70421
|
}
|
|
69925
70422
|
});
|
|
69926
70423
|
try {
|
|
69927
70424
|
await this.tmux.sendKey(resolved.sessionName, "Escape");
|
|
69928
|
-
await sleep(150);
|
|
69929
70425
|
} catch {}
|
|
69930
70426
|
}
|
|
69931
70427
|
return {
|
|
@@ -69941,7 +70437,7 @@ class RunnerService {
|
|
|
69941
70437
|
const existed = await this.tmux.hasSession(resolved.sessionName);
|
|
69942
70438
|
if (existed) {
|
|
69943
70439
|
await this.tmux.sendKey(resolved.sessionName, "Enter");
|
|
69944
|
-
await this.
|
|
70440
|
+
await this.sessionMapping.touch(resolved);
|
|
69945
70441
|
}
|
|
69946
70442
|
return {
|
|
69947
70443
|
agentId: resolved.agentId,
|
|
@@ -69976,6 +70472,7 @@ class RunnerService {
|
|
|
69976
70472
|
if (!await this.tmux.hasSession(resolved.sessionName)) {
|
|
69977
70473
|
throw new Error(`tmux session "${resolved.sessionName}" does not exist`);
|
|
69978
70474
|
}
|
|
70475
|
+
await this.acceptWorkspaceTrustPromptIfPresent(resolved);
|
|
69979
70476
|
await submitTmuxSessionInput({
|
|
69980
70477
|
tmux: this.tmux,
|
|
69981
70478
|
sessionName: resolved.sessionName,
|
|
@@ -69983,7 +70480,7 @@ class RunnerService {
|
|
|
69983
70480
|
promptSubmitDelayMs: resolved.runner.promptSubmitDelayMs,
|
|
69984
70481
|
timingContext: undefined
|
|
69985
70482
|
});
|
|
69986
|
-
await this.
|
|
70483
|
+
await this.sessionMapping.touch(resolved);
|
|
69987
70484
|
return {
|
|
69988
70485
|
agentId: resolved.agentId,
|
|
69989
70486
|
sessionKey: resolved.sessionKey,
|
|
@@ -70048,12 +70545,20 @@ async function monitorTmuxRun(params) {
|
|
|
70048
70545
|
let previousRenderedRunningSnapshot = "";
|
|
70049
70546
|
let lastPaneChangeAt = params.startedAt;
|
|
70050
70547
|
let sawActivity = false;
|
|
70051
|
-
let sawPaneChange =
|
|
70548
|
+
let sawPaneChange = Boolean(normalizePaneText(params.initialSnapshot));
|
|
70052
70549
|
let sawPromptSubmission = Boolean(params.prompt);
|
|
70053
70550
|
let detachedNotified = params.detachedAlready;
|
|
70054
70551
|
let firstMeaningfulDeltaLogged = false;
|
|
70055
70552
|
let noOutputThresholdLogged = false;
|
|
70056
70553
|
if (params.prompt) {
|
|
70554
|
+
if (params.trustWorkspace) {
|
|
70555
|
+
await acceptTmuxTrustPromptIfPresent({
|
|
70556
|
+
tmux: params.tmux,
|
|
70557
|
+
sessionName: params.sessionName,
|
|
70558
|
+
captureLines: params.captureLines,
|
|
70559
|
+
startupDelayMs: params.startupDelayMs ?? 0
|
|
70560
|
+
});
|
|
70561
|
+
}
|
|
70057
70562
|
logLatencyDebug("tmux-submit-start", params.timingContext, {
|
|
70058
70563
|
sessionName: params.sessionName,
|
|
70059
70564
|
promptSubmitDelayMs: params.promptSubmitDelayMs
|
|
@@ -70193,6 +70698,12 @@ function isRetryableObserverDeliveryError(error) {
|
|
|
70193
70698
|
const message = error instanceof Error ? `${error.name} ${error.message}` : String(error);
|
|
70194
70699
|
return /fetch failed|request timed out|network|socket hang up/i.test(message);
|
|
70195
70700
|
}
|
|
70701
|
+
function buildMissingSessionIdStartupWarning() {
|
|
70702
|
+
return [
|
|
70703
|
+
"Runner session started, but clisbot could not capture a durable session id yet.",
|
|
70704
|
+
"This session is running, but it is not resumable until a session id is captured and persisted."
|
|
70705
|
+
].join(" ");
|
|
70706
|
+
}
|
|
70196
70707
|
|
|
70197
70708
|
class ActiveRunInProgressError extends Error {
|
|
70198
70709
|
update;
|
|
@@ -70247,15 +70758,40 @@ class SessionService {
|
|
|
70247
70758
|
this.resolveTarget = resolveTarget;
|
|
70248
70759
|
}
|
|
70249
70760
|
async recoverPersistedRuns() {
|
|
70761
|
+
const activeSessionKeys = new Set;
|
|
70250
70762
|
const entries = await this.sessionState.listEntries();
|
|
70251
70763
|
for (const entry of entries) {
|
|
70252
70764
|
if (!entry.runtime || entry.runtime.state === "idle") {
|
|
70253
70765
|
continue;
|
|
70254
70766
|
}
|
|
70255
|
-
await this.reconcilePersistedActiveRun({
|
|
70767
|
+
const run = await this.reconcilePersistedActiveRun({
|
|
70256
70768
|
agentId: entry.agentId,
|
|
70257
70769
|
sessionKey: entry.sessionKey
|
|
70258
70770
|
});
|
|
70771
|
+
if (run) {
|
|
70772
|
+
activeSessionKeys.add(run.resolved.sessionKey);
|
|
70773
|
+
}
|
|
70774
|
+
}
|
|
70775
|
+
return activeSessionKeys;
|
|
70776
|
+
}
|
|
70777
|
+
async clearLostPersistedActiveRuns() {
|
|
70778
|
+
const entries = await this.sessionState.listEntries();
|
|
70779
|
+
for (const entry of entries) {
|
|
70780
|
+
if (!entry.runtime || entry.runtime.state === "idle") {
|
|
70781
|
+
continue;
|
|
70782
|
+
}
|
|
70783
|
+
if (this.activeRuns.has(entry.sessionKey)) {
|
|
70784
|
+
continue;
|
|
70785
|
+
}
|
|
70786
|
+
const resolved = this.resolveTarget({
|
|
70787
|
+
agentId: entry.agentId,
|
|
70788
|
+
sessionKey: entry.sessionKey
|
|
70789
|
+
});
|
|
70790
|
+
if (!await this.tmux.hasSession(resolved.sessionName)) {
|
|
70791
|
+
await this.sessionState.setSessionRuntime(resolved, {
|
|
70792
|
+
state: "idle"
|
|
70793
|
+
});
|
|
70794
|
+
}
|
|
70259
70795
|
}
|
|
70260
70796
|
}
|
|
70261
70797
|
async executePrompt(target, prompt, observer, options = {}) {
|
|
@@ -70310,6 +70846,7 @@ class SessionService {
|
|
|
70310
70846
|
throw new Error(`Active run disappeared during startup for ${provisionalResolved.sessionKey}.`);
|
|
70311
70847
|
}
|
|
70312
70848
|
run.resolved = resolved;
|
|
70849
|
+
run.sessionId = (await this.sessionState.getEntry(resolved.sessionKey))?.sessionId?.trim() || undefined;
|
|
70313
70850
|
run.startedAt = startedAt;
|
|
70314
70851
|
run.latestUpdate = this.createRunUpdate({
|
|
70315
70852
|
resolved,
|
|
@@ -70322,6 +70859,17 @@ class SessionService {
|
|
|
70322
70859
|
state: "running",
|
|
70323
70860
|
startedAt
|
|
70324
70861
|
});
|
|
70862
|
+
if (!run.sessionId) {
|
|
70863
|
+
await this.notifyRunObservers(run, this.createRunUpdate({
|
|
70864
|
+
resolved,
|
|
70865
|
+
status: "running",
|
|
70866
|
+
snapshot: "",
|
|
70867
|
+
fullSnapshot: initialSnapshot,
|
|
70868
|
+
initialSnapshot,
|
|
70869
|
+
note: buildMissingSessionIdStartupWarning(),
|
|
70870
|
+
forceVisible: true
|
|
70871
|
+
}));
|
|
70872
|
+
}
|
|
70325
70873
|
this.startRunMonitor(resolved.sessionKey, {
|
|
70326
70874
|
runId,
|
|
70327
70875
|
prompt,
|
|
@@ -70392,6 +70940,34 @@ class SessionService {
|
|
|
70392
70940
|
detached: true
|
|
70393
70941
|
};
|
|
70394
70942
|
}
|
|
70943
|
+
async interruptActiveRun(target) {
|
|
70944
|
+
const run = this.activeRuns.get(target.sessionKey) ?? await this.reconcilePersistedActiveRun(target);
|
|
70945
|
+
if (!run) {
|
|
70946
|
+
return {
|
|
70947
|
+
interrupted: false
|
|
70948
|
+
};
|
|
70949
|
+
}
|
|
70950
|
+
const error = new Error("Run interrupted by /stop.");
|
|
70951
|
+
const update = this.createRunUpdate({
|
|
70952
|
+
resolved: run.resolved,
|
|
70953
|
+
status: "error",
|
|
70954
|
+
snapshot: error.message,
|
|
70955
|
+
fullSnapshot: run.latestUpdate.fullSnapshot,
|
|
70956
|
+
initialSnapshot: run.latestUpdate.initialSnapshot,
|
|
70957
|
+
note: "Run interrupted."
|
|
70958
|
+
});
|
|
70959
|
+
await this.sessionState.setSessionRuntime(run.resolved, {
|
|
70960
|
+
state: "idle"
|
|
70961
|
+
});
|
|
70962
|
+
await this.notifyRunObservers(run, update);
|
|
70963
|
+
if (!run.initialResult.settled) {
|
|
70964
|
+
run.initialResult.reject(error);
|
|
70965
|
+
}
|
|
70966
|
+
this.activeRuns.delete(run.resolved.sessionKey);
|
|
70967
|
+
return {
|
|
70968
|
+
interrupted: true
|
|
70969
|
+
};
|
|
70970
|
+
}
|
|
70395
70971
|
hasActiveRun(target) {
|
|
70396
70972
|
return this.activeRuns.has(target.sessionKey);
|
|
70397
70973
|
}
|
|
@@ -70439,6 +71015,17 @@ class SessionService {
|
|
|
70439
71015
|
}
|
|
70440
71016
|
this.activeRuns.clear();
|
|
70441
71017
|
}
|
|
71018
|
+
listLiveSessionRuntimes() {
|
|
71019
|
+
return [...this.activeRuns.values()].map((run) => ({
|
|
71020
|
+
state: run.latestUpdate.status === "detached" ? "detached" : "running",
|
|
71021
|
+
startedAt: run.startedAt,
|
|
71022
|
+
sessionKey: run.resolved.sessionKey,
|
|
71023
|
+
agentId: run.resolved.agentId
|
|
71024
|
+
}));
|
|
71025
|
+
}
|
|
71026
|
+
getLiveSessionId(target) {
|
|
71027
|
+
return this.activeRuns.get(target.sessionKey)?.sessionId;
|
|
71028
|
+
}
|
|
70442
71029
|
buildDetachedNote(resolved) {
|
|
70443
71030
|
return `This session has been running for over ${resolved.stream.maxRuntimeLabel}. clisbot left it running and will post the final result here when it completes. Use \`/attach\` for live updates, \`/watch every <duration>\` for periodic updates, or \`/stop\` to interrupt it.`;
|
|
70444
71031
|
}
|
|
@@ -70641,6 +71228,7 @@ class SessionService {
|
|
|
70641
71228
|
return true;
|
|
70642
71229
|
}
|
|
70643
71230
|
currentRun.resolved = recovered.resolved;
|
|
71231
|
+
currentRun.sessionId = (await this.sessionState.getEntry(recovered.resolved.sessionKey))?.sessionId?.trim() || currentRun.sessionId;
|
|
70644
71232
|
currentRun.latestUpdate = this.createRunUpdate({
|
|
70645
71233
|
resolved: currentRun.resolved,
|
|
70646
71234
|
status: currentRun.latestUpdate.status === "detached" ? "detached" : "running",
|
|
@@ -70674,7 +71262,7 @@ class SessionService {
|
|
|
70674
71262
|
if (!currentRun) {
|
|
70675
71263
|
return true;
|
|
70676
71264
|
}
|
|
70677
|
-
if (await this.
|
|
71265
|
+
if (await this.requiresManualNewAfterFailedResume(currentRun)) {
|
|
70678
71266
|
await this.notifyRecoveryStep(currentRun, buildRunRecoveryNote("resume-failed"));
|
|
70679
71267
|
await this.failActiveRun(sessionKey, currentRun.runId, new Error(buildRunRecoveryNote("manual-new-required")));
|
|
70680
71268
|
return true;
|
|
@@ -70690,12 +71278,13 @@ class SessionService {
|
|
|
70690
71278
|
return true;
|
|
70691
71279
|
}
|
|
70692
71280
|
}
|
|
70693
|
-
async
|
|
70694
|
-
|
|
71281
|
+
async requiresManualNewAfterFailedResume(run) {
|
|
71282
|
+
const storedSessionId = (await this.sessionState.getEntry(run.resolved.sessionKey))?.sessionId?.trim() || "";
|
|
71283
|
+
if (!storedSessionId) {
|
|
70695
71284
|
return false;
|
|
70696
71285
|
}
|
|
70697
|
-
|
|
70698
|
-
return
|
|
71286
|
+
run.sessionId = storedSessionId;
|
|
71287
|
+
return true;
|
|
70699
71288
|
}
|
|
70700
71289
|
startRunMonitor(sessionKey, params) {
|
|
70701
71290
|
const run = this.getRun(sessionKey, params.runId);
|
|
@@ -70712,6 +71301,8 @@ class SessionService {
|
|
|
70712
71301
|
sessionName: run.resolved.sessionName,
|
|
70713
71302
|
prompt: params.prompt,
|
|
70714
71303
|
promptSubmitDelayMs: run.resolved.runner.promptSubmitDelayMs,
|
|
71304
|
+
trustWorkspace: run.resolved.runner.trustWorkspace,
|
|
71305
|
+
startupDelayMs: run.resolved.runner.startupDelayMs,
|
|
70715
71306
|
captureLines: run.resolved.stream.captureLines,
|
|
70716
71307
|
updateIntervalMs: run.resolved.stream.updateIntervalMs,
|
|
70717
71308
|
idleTimeoutMs: run.resolved.stream.idleTimeoutMs,
|
|
@@ -70857,6 +71448,7 @@ class SessionService {
|
|
|
70857
71448
|
const run = {
|
|
70858
71449
|
runId,
|
|
70859
71450
|
resolved,
|
|
71451
|
+
sessionId: (await this.sessionState.getEntry(resolved.sessionKey))?.sessionId?.trim() || undefined,
|
|
70860
71452
|
observers: new Map,
|
|
70861
71453
|
observerFailures: new Map,
|
|
70862
71454
|
initialResult,
|
|
@@ -71071,14 +71663,9 @@ var EMPTY_PROGRESS_PHRASE = "";
|
|
|
71071
71663
|
var PROGRESS_FLAG_SUFFIX = "|progress";
|
|
71072
71664
|
var EMPTY_PROGRESS_FLAG_SUFFIX = "";
|
|
71073
71665
|
var PROGRESS_RULES_BLOCK = `- use that command to send progress updates and the final reply back to the conversation
|
|
71074
|
-
- send at most {{max_progress_messages}} progress updates
|
|
71075
|
-
- keep progress updates short and meaningful
|
|
71076
|
-
- do not send progress updates for trivial internal steps
|
|
71077
|
-
`;
|
|
71078
|
-
var FINAL_ONLY_RULES_BLOCK = `- use that command only for the final user-facing reply
|
|
71079
|
-
- do not send user-facing progress updates for this conversation
|
|
71666
|
+
- send at most {{max_progress_messages}} short, meaningful progress updates; skip trivial internal steps
|
|
71080
71667
|
`;
|
|
71081
|
-
var FINAL_RULE_REQUIRED = "send
|
|
71668
|
+
var FINAL_RULE_REQUIRED = "send a single final user-facing message by default; split only when channel limits require it or clarity would otherwise suffer";
|
|
71082
71669
|
var FINAL_RULE_OPTIONAL = "final response is optional";
|
|
71083
71670
|
var EMPTY_REPLY_COMMAND = "";
|
|
71084
71671
|
var EMPTY_REPLY_RULES = "";
|
|
@@ -71096,8 +71683,10 @@ var TELEGRAM_REPLY_COMMAND_BASE = `{{command}} message send \\
|
|
|
71096
71683
|
--render native \\
|
|
71097
71684
|
`;
|
|
71098
71685
|
var SLACK_REPLY_STYLE_HINT = `Put readable hierarchical Markdown in the --message body.
|
|
71686
|
+
For clickable links, use canonical URLs and do not wrap them in backticks.
|
|
71099
71687
|
Keep each paragraph, list, or code block under 2500 chars.`;
|
|
71100
71688
|
var TELEGRAM_REPLY_STYLE_HINT = `Put readable hierarchical Markdown in the --message body.
|
|
71689
|
+
For clickable links, use canonical URLs and do not wrap them in backticks.
|
|
71101
71690
|
Keep the Markdown body under 3000 chars.`;
|
|
71102
71691
|
var ACCOUNT_CLAUSE = " --account {{account_id}} \\\n";
|
|
71103
71692
|
var EMPTY_ACCOUNT_CLAUSE = "";
|
|
@@ -71138,7 +71727,8 @@ function buildChannelPromptText(params) {
|
|
|
71138
71727
|
identity: params.identity,
|
|
71139
71728
|
config: params.config,
|
|
71140
71729
|
responseMode: params.responseMode,
|
|
71141
|
-
streaming: params.streaming
|
|
71730
|
+
streaming: params.streaming,
|
|
71731
|
+
maxProgressMessagesOverride: params.maxProgressMessagesOverride
|
|
71142
71732
|
});
|
|
71143
71733
|
const context = resolvePromptContext(params);
|
|
71144
71734
|
return renderTemplate(BASE_TEMPLATE, {
|
|
@@ -71183,10 +71773,11 @@ function renderMessagePromptParts(params) {
|
|
|
71183
71773
|
replyStyleHint: EMPTY_REPLY_STYLE_HINT
|
|
71184
71774
|
};
|
|
71185
71775
|
}
|
|
71186
|
-
const
|
|
71187
|
-
const
|
|
71188
|
-
const
|
|
71189
|
-
const
|
|
71776
|
+
const maxProgressMessages = Math.max(0, params.maxProgressMessagesOverride ?? params.config.maxProgressMessages);
|
|
71777
|
+
const progressEnabled = maxProgressMessages > 0;
|
|
71778
|
+
const progressPhrase = progressEnabled ? PROGRESS_PHRASE : EMPTY_PROGRESS_PHRASE;
|
|
71779
|
+
const progressFlagSuffix = progressEnabled ? PROGRESS_FLAG_SUFFIX : EMPTY_PROGRESS_FLAG_SUFFIX;
|
|
71780
|
+
const progressRulesBlock = progressEnabled ? PROGRESS_RULES_BLOCK : "";
|
|
71190
71781
|
const finalRuleLine = params.config.requireFinalResponse ? FINAL_RULE_REQUIRED : FINAL_RULE_OPTIONAL;
|
|
71191
71782
|
return {
|
|
71192
71783
|
deliveryIntro: renderTemplate(DELIVERY_INTRO, {
|
|
@@ -71201,7 +71792,7 @@ function renderMessagePromptParts(params) {
|
|
|
71201
71792
|
}),
|
|
71202
71793
|
replyRules: renderTemplate(REPLY_RULES, {
|
|
71203
71794
|
progress_rules_block: renderTemplate(progressRulesBlock, {
|
|
71204
|
-
max_progress_messages: String(
|
|
71795
|
+
max_progress_messages: String(maxProgressMessages)
|
|
71205
71796
|
}),
|
|
71206
71797
|
final_rule_line: finalRuleLine
|
|
71207
71798
|
}),
|
|
@@ -71215,8 +71806,9 @@ function renderConfigurationGuidance() {
|
|
|
71215
71806
|
const cliName = getRenderedCliName();
|
|
71216
71807
|
return [
|
|
71217
71808
|
`When the user asks to change ${cliName} configuration, use ${cliName} CLI commands; see ${renderCliCommand("--help", { inline: true })}, ${renderCliCommand("bots --help", { inline: true })}, ${renderCliCommand("routes --help", { inline: true })}, ${renderCliCommand("auth --help", { inline: true })}, or ${renderCliCommand("update --help", { inline: true })} for details.`,
|
|
71809
|
+
`For ${cliName} install or update requests, check ${renderCliCommand("update --help", { inline: true })} first and follow it.`,
|
|
71218
71810
|
`For schedule/loop/reminder requests, inspect ${renderCliCommand("loops --help", { inline: true })} and use the loops CLI.`,
|
|
71219
|
-
`For durable queue
|
|
71811
|
+
`For durable queue requests, inspect ${renderCliCommand("queues --help", { inline: true })} and use the queues CLI.`
|
|
71220
71812
|
].join(`
|
|
71221
71813
|
`);
|
|
71222
71814
|
}
|
|
@@ -71266,9 +71858,11 @@ function buildReplyCommandBase(params) {
|
|
|
71266
71858
|
}
|
|
71267
71859
|
|
|
71268
71860
|
// src/channels/mode-config-shared.ts
|
|
71269
|
-
function createTelegramRouteOverride() {
|
|
71861
|
+
function createTelegramRouteOverride(base) {
|
|
71270
71862
|
return {
|
|
71271
|
-
enabled: true,
|
|
71863
|
+
enabled: base?.enabled ?? true,
|
|
71864
|
+
requireMention: base?.requireMention ?? true,
|
|
71865
|
+
allowBots: base?.allowBots ?? false,
|
|
71272
71866
|
allowUsers: [],
|
|
71273
71867
|
blockUsers: []
|
|
71274
71868
|
};
|
|
@@ -71291,7 +71885,7 @@ function getOrCreateTelegramTopicRoute(bot, chatId, topicId) {
|
|
|
71291
71885
|
if (existingTopic) {
|
|
71292
71886
|
return existingTopic;
|
|
71293
71887
|
}
|
|
71294
|
-
const createdTopic = createTelegramRouteOverride();
|
|
71888
|
+
const createdTopic = createTelegramRouteOverride(group);
|
|
71295
71889
|
group.topics[topicId] = createdTopic;
|
|
71296
71890
|
return createdTopic;
|
|
71297
71891
|
}
|
|
@@ -71722,8 +72316,9 @@ class SurfaceRuntime {
|
|
|
71722
72316
|
}
|
|
71723
72317
|
const identity = this.buildLoopChannelIdentity(loop);
|
|
71724
72318
|
const notifications = this.resolveSurfaceNotifications(identity);
|
|
72319
|
+
const mode = loop.loopStart ?? notifications.loopStart;
|
|
71725
72320
|
const text = loop.kind === "calendar" ? renderLoopStartNotification({
|
|
71726
|
-
mode
|
|
72321
|
+
mode,
|
|
71727
72322
|
agentId: target.agentId,
|
|
71728
72323
|
loopId: loop.id,
|
|
71729
72324
|
promptSummary: loop.promptSummary,
|
|
@@ -71736,7 +72331,7 @@ class SurfaceRuntime {
|
|
|
71736
72331
|
maxRuns: loop.maxRuns,
|
|
71737
72332
|
kind: "calendar"
|
|
71738
72333
|
}) : renderLoopStartNotification({
|
|
71739
|
-
mode
|
|
72334
|
+
mode,
|
|
71740
72335
|
agentId: target.agentId,
|
|
71741
72336
|
loopId: loop.id,
|
|
71742
72337
|
promptSummary: loop.promptSummary,
|
|
@@ -71824,7 +72419,7 @@ class SurfaceRuntime {
|
|
|
71824
72419
|
}
|
|
71825
72420
|
}
|
|
71826
72421
|
async buildManagedLoopPrompt(agentId, loop) {
|
|
71827
|
-
if (!loop.
|
|
72422
|
+
if (!loop.surfaceBinding) {
|
|
71828
72423
|
return loop.promptText;
|
|
71829
72424
|
}
|
|
71830
72425
|
const identity = this.buildLoopChannelIdentity(loop);
|
|
@@ -71839,7 +72434,7 @@ class SurfaceRuntime {
|
|
|
71839
72434
|
scheduledLoopId: loop.id
|
|
71840
72435
|
});
|
|
71841
72436
|
return buildAgentPromptText({
|
|
71842
|
-
text: loop.
|
|
72437
|
+
text: loop.promptText,
|
|
71843
72438
|
identity,
|
|
71844
72439
|
config: channelConfig.agentPrompt,
|
|
71845
72440
|
cliTool: getAgentEntry2(this.loadedConfig, agentId)?.cli,
|
|
@@ -71849,11 +72444,12 @@ class SurfaceRuntime {
|
|
|
71849
72444
|
agentId,
|
|
71850
72445
|
time: promptTime,
|
|
71851
72446
|
promptContext,
|
|
71852
|
-
scheduledLoopId: loop.id
|
|
72447
|
+
scheduledLoopId: loop.id,
|
|
72448
|
+
maxProgressMessagesOverride: loop.progressMessages
|
|
71853
72449
|
});
|
|
71854
72450
|
}
|
|
71855
72451
|
async buildManagedQueuePrompt(agentId, item) {
|
|
71856
|
-
if (!item.
|
|
72452
|
+
if (!item.surfaceBinding) {
|
|
71857
72453
|
return item.promptText;
|
|
71858
72454
|
}
|
|
71859
72455
|
const identity = this.buildQueueChannelIdentity(item);
|
|
@@ -71867,7 +72463,7 @@ class SurfaceRuntime {
|
|
|
71867
72463
|
time: promptTime
|
|
71868
72464
|
});
|
|
71869
72465
|
return buildAgentPromptText({
|
|
71870
|
-
text: item.
|
|
72466
|
+
text: item.promptText,
|
|
71871
72467
|
identity,
|
|
71872
72468
|
config: channelConfig.agentPrompt,
|
|
71873
72469
|
cliTool: getAgentEntry2(this.loadedConfig, agentId)?.cli,
|
|
@@ -72034,6 +72630,7 @@ class AgentService {
|
|
|
72034
72630
|
tmuxClient;
|
|
72035
72631
|
queue = new AgentJobQueue;
|
|
72036
72632
|
sessionState;
|
|
72633
|
+
sessionMapping;
|
|
72037
72634
|
runnerSessions;
|
|
72038
72635
|
activeRuns;
|
|
72039
72636
|
managedLoops;
|
|
@@ -72048,7 +72645,8 @@ class AgentService {
|
|
|
72048
72645
|
this.tmuxClient = deps.tmux ?? new TmuxClient(this.loadedConfig.raw.tmux.socketPath);
|
|
72049
72646
|
const sessionStore = deps.sessionStore ?? new SessionStore(resolveSessionStorePath(this.loadedConfig));
|
|
72050
72647
|
this.sessionState = new AgentSessionState(sessionStore);
|
|
72051
|
-
this.
|
|
72648
|
+
this.sessionMapping = new SessionMapping(this.sessionState);
|
|
72649
|
+
this.runnerSessions = new RunnerService(this.loadedConfig, this.tmuxClient, this.sessionState, (target) => this.resolveTarget(target), this.sessionMapping);
|
|
72052
72650
|
this.activeRuns = this.createSessionService();
|
|
72053
72651
|
this.surfaceRuntime = new SurfaceRuntime(this.loadedConfig);
|
|
72054
72652
|
this.managedQueues = this.createManagedQueueController();
|
|
@@ -72059,7 +72657,7 @@ class AgentService {
|
|
|
72059
72657
|
}
|
|
72060
72658
|
set tmux(value) {
|
|
72061
72659
|
this.tmuxClient = value;
|
|
72062
|
-
this.runnerSessions = new RunnerService(this.loadedConfig, this.tmuxClient, this.sessionState, (target) => this.resolveTarget(target));
|
|
72660
|
+
this.runnerSessions = new RunnerService(this.loadedConfig, this.tmuxClient, this.sessionState, (target) => this.resolveTarget(target), this.sessionMapping);
|
|
72063
72661
|
this.activeRuns = this.createSessionService();
|
|
72064
72662
|
this.managedQueues = this.createManagedQueueController();
|
|
72065
72663
|
}
|
|
@@ -72090,9 +72688,8 @@ class AgentService {
|
|
|
72090
72688
|
});
|
|
72091
72689
|
}
|
|
72092
72690
|
async start() {
|
|
72093
|
-
await this.activeRuns.recoverPersistedRuns();
|
|
72094
|
-
|
|
72095
|
-
await this.sessionState.resetStaleRunningQueuedItems(activeSessions);
|
|
72691
|
+
const activeSessionKeys = await this.activeRuns.recoverPersistedRuns();
|
|
72692
|
+
await this.sessionState.resetStaleRunningQueuedItems(activeSessionKeys);
|
|
72096
72693
|
await this.managedQueues.reconcilePersistedQueueItems();
|
|
72097
72694
|
this.queueReconcileTimer = setInterval(() => {
|
|
72098
72695
|
this.managedQueues.reconcilePersistedQueueItems().catch((error) => {
|
|
@@ -72150,7 +72747,12 @@ class AgentService {
|
|
|
72150
72747
|
return this.runnerSessions.captureTranscript(target);
|
|
72151
72748
|
}
|
|
72152
72749
|
async interruptSession(target) {
|
|
72153
|
-
|
|
72750
|
+
const runner = await this.runnerSessions.interruptSession(target);
|
|
72751
|
+
const activeRun = await this.activeRuns.interruptActiveRun(target);
|
|
72752
|
+
return {
|
|
72753
|
+
...runner,
|
|
72754
|
+
interrupted: runner.interrupted || activeRun.interrupted
|
|
72755
|
+
};
|
|
72154
72756
|
}
|
|
72155
72757
|
async nudgeSession(target) {
|
|
72156
72758
|
return this.runnerSessions.nudgeSession(target);
|
|
@@ -72164,17 +72766,29 @@ class AgentService {
|
|
|
72164
72766
|
async getSessionRuntime(target) {
|
|
72165
72767
|
return this.sessionState.getSessionRuntime(target);
|
|
72166
72768
|
}
|
|
72769
|
+
async getSessionEntry(target) {
|
|
72770
|
+
return this.sessionState.getEntry(target.sessionKey);
|
|
72771
|
+
}
|
|
72772
|
+
async listSessionEntries() {
|
|
72773
|
+
return this.sessionState.listEntries();
|
|
72774
|
+
}
|
|
72167
72775
|
async getSessionDiagnostics(target) {
|
|
72168
72776
|
const resolved = this.resolveTarget(target);
|
|
72169
72777
|
const entry = await this.sessionState.getEntry(target.sessionKey);
|
|
72170
|
-
const
|
|
72778
|
+
const liveSessionId = this.activeRuns.getLiveSessionId(target);
|
|
72779
|
+
const storedSessionId = entry?.sessionId?.trim() || undefined;
|
|
72780
|
+
const sessionId = liveSessionId ?? storedSessionId;
|
|
72171
72781
|
return {
|
|
72782
|
+
sessionName: resolved.sessionName,
|
|
72172
72783
|
sessionId,
|
|
72784
|
+
sessionIdPersistence: sessionId && sessionId === storedSessionId ? "persisted" : sessionId ? "not-persisted-yet" : undefined,
|
|
72785
|
+
storedSessionId,
|
|
72173
72786
|
resumeCommand: buildResumeCommandPreview(resolved, sessionId)
|
|
72174
72787
|
};
|
|
72175
72788
|
}
|
|
72176
|
-
async
|
|
72177
|
-
|
|
72789
|
+
async listLiveSessionRuntimes() {
|
|
72790
|
+
await this.activeRuns.clearLostPersistedActiveRuns();
|
|
72791
|
+
return this.activeRuns.listLiveSessionRuntimes();
|
|
72178
72792
|
}
|
|
72179
72793
|
async setConversationFollowUpMode(target, mode) {
|
|
72180
72794
|
return this.sessionState.setConversationFollowUpMode(this.resolveTarget(target), mode);
|
|
@@ -72275,8 +72889,8 @@ class AgentService {
|
|
|
72275
72889
|
async createCalendarLoop(params) {
|
|
72276
72890
|
return this.managedLoops.createCalendarLoop(params);
|
|
72277
72891
|
}
|
|
72278
|
-
async cancelIntervalLoop(loopId) {
|
|
72279
|
-
return this.managedLoops.cancelIntervalLoop(loopId);
|
|
72892
|
+
async cancelIntervalLoop(target, loopId) {
|
|
72893
|
+
return this.managedLoops.cancelIntervalLoop(target, loopId);
|
|
72280
72894
|
}
|
|
72281
72895
|
async cancelIntervalLoopsForSession(target) {
|
|
72282
72896
|
return this.managedLoops.cancelIntervalLoopsForSession(target);
|
|
@@ -72287,8 +72901,8 @@ class AgentService {
|
|
|
72287
72901
|
listIntervalLoops(params) {
|
|
72288
72902
|
return this.managedLoops.listIntervalLoops(params);
|
|
72289
72903
|
}
|
|
72290
|
-
getIntervalLoop(loopId) {
|
|
72291
|
-
return this.managedLoops.getIntervalLoop(loopId);
|
|
72904
|
+
getIntervalLoop(target, loopId) {
|
|
72905
|
+
return this.managedLoops.getIntervalLoop(target.sessionKey, loopId);
|
|
72292
72906
|
}
|
|
72293
72907
|
getActiveIntervalLoopCount() {
|
|
72294
72908
|
return this.managedLoops.getActiveIntervalLoopCount();
|
|
@@ -72340,9 +72954,8 @@ function createStoredQueueItem(params) {
|
|
|
72340
72954
|
createdAt: now,
|
|
72341
72955
|
updatedAt: now,
|
|
72342
72956
|
promptText: params.promptText,
|
|
72343
|
-
canonicalPromptText: params.canonicalPromptText,
|
|
72344
72957
|
protectedControlMutationRule: params.protectedControlMutationRule,
|
|
72345
|
-
promptSummary: params.promptSummary ?? summarizeQueuePrompt(params.
|
|
72958
|
+
promptSummary: params.promptSummary ?? summarizeQueuePrompt(params.promptText),
|
|
72346
72959
|
promptSource: "custom",
|
|
72347
72960
|
createdBy: params.createdBy,
|
|
72348
72961
|
sender: params.sender,
|
|
@@ -72593,20 +73206,13 @@ function renderStartupSteeringUnavailableMessage() {
|
|
|
72593
73206
|
].join(`
|
|
72594
73207
|
`);
|
|
72595
73208
|
}
|
|
72596
|
-
function
|
|
72597
|
-
|
|
72598
|
-
|
|
72599
|
-
|
|
72600
|
-
|
|
72601
|
-
|
|
72602
|
-
|
|
72603
|
-
if (identity.senderId) {
|
|
72604
|
-
return `${identity.platform}:${identity.senderId}`;
|
|
72605
|
-
}
|
|
72606
|
-
if (identity.platform === "slack") {
|
|
72607
|
-
return "slack:U123ABC456";
|
|
72608
|
-
}
|
|
72609
|
-
return "telegram:1276408333";
|
|
73209
|
+
function renderNewSessionFailureMessage(error) {
|
|
73210
|
+
const details = error instanceof Error && error.message.trim() ? error.message.trim() : "Unknown error.";
|
|
73211
|
+
return [
|
|
73212
|
+
"Could not finish opening a new runner conversation.",
|
|
73213
|
+
details
|
|
73214
|
+
].join(`
|
|
73215
|
+
`);
|
|
72610
73216
|
}
|
|
72611
73217
|
function renderWhoAmIMessage(params) {
|
|
72612
73218
|
const lines = [
|
|
@@ -72615,9 +73221,13 @@ function renderWhoAmIMessage(params) {
|
|
|
72615
73221
|
`platform: \`${params.identity.platform}\``,
|
|
72616
73222
|
`conversationKind: \`${params.identity.conversationKind}\``,
|
|
72617
73223
|
`agentId: \`${params.route.agentId}\``,
|
|
72618
|
-
`
|
|
72619
|
-
`
|
|
73224
|
+
`sessionName: \`${params.sessionDiagnostics.sessionName ?? "(not available)"}\``,
|
|
73225
|
+
`sessionId: \`${params.sessionDiagnostics.sessionId ?? "(not available yet)"}\``,
|
|
73226
|
+
`sessionIdPersistence: \`${params.sessionDiagnostics.sessionIdPersistence ?? "not stored yet"}\``
|
|
72620
73227
|
];
|
|
73228
|
+
if (params.sessionDiagnostics.storedSessionId && params.sessionDiagnostics.storedSessionId !== params.sessionDiagnostics.sessionId) {
|
|
73229
|
+
lines.push(`storedSessionId: \`${params.sessionDiagnostics.storedSessionId}\``);
|
|
73230
|
+
}
|
|
72621
73231
|
if (params.identity.senderId) {
|
|
72622
73232
|
lines.push(`senderId: \`${params.identity.senderId}\``);
|
|
72623
73233
|
}
|
|
@@ -72633,7 +73243,7 @@ function renderWhoAmIMessage(params) {
|
|
|
72633
73243
|
if (params.identity.topicId) {
|
|
72634
73244
|
lines.push(`topicId: \`${params.identity.topicId}\``);
|
|
72635
73245
|
}
|
|
72636
|
-
lines.push(`resumeCommand: \`${params.sessionDiagnostics.resumeCommand ?? "(not available yet)"}\``, `principal: \`${params.auth.principal ?? "(none)"}\``, `
|
|
73246
|
+
lines.push(`resumeCommand: \`${params.sessionDiagnostics.resumeCommand ?? "(not available yet)"}\``, `principal: \`${params.auth.principal ?? "(none)"}\``, `appRole: \`${params.auth.appRole}\``, `agentRole: \`${params.auth.agentRole}\``, `mayBypassPairing: \`${params.auth.mayBypassPairing}\``, `mayBypassSharedSenderPolicy: \`${params.auth.mayBypassSharedSenderPolicy}\``, `mayManageProtectedResources: \`${params.auth.mayManageProtectedResources}\``, `canUseShell: \`${params.auth.canUseShell}\``, `verbose: \`${params.route.verbose}\``);
|
|
72637
73247
|
return lines.join(`
|
|
72638
73248
|
`);
|
|
72639
73249
|
}
|
|
@@ -72644,9 +73254,13 @@ function renderRouteStatusMessage(params) {
|
|
|
72644
73254
|
`platform: \`${params.identity.platform}\``,
|
|
72645
73255
|
`conversationKind: \`${params.identity.conversationKind}\``,
|
|
72646
73256
|
`agentId: \`${params.route.agentId}\``,
|
|
72647
|
-
`
|
|
72648
|
-
`
|
|
73257
|
+
`sessionName: \`${params.sessionDiagnostics.sessionName ?? "(not available)"}\``,
|
|
73258
|
+
`sessionId: \`${params.sessionDiagnostics.sessionId ?? "(not available yet)"}\``,
|
|
73259
|
+
`sessionIdPersistence: \`${params.sessionDiagnostics.sessionIdPersistence ?? "not stored yet"}\``
|
|
72649
73260
|
];
|
|
73261
|
+
if (params.sessionDiagnostics.storedSessionId && params.sessionDiagnostics.storedSessionId !== params.sessionDiagnostics.sessionId) {
|
|
73262
|
+
lines.push(`storedSessionId: \`${params.sessionDiagnostics.storedSessionId}\``);
|
|
73263
|
+
}
|
|
72650
73264
|
if (params.identity.senderId) {
|
|
72651
73265
|
lines.push(`senderId: \`${params.identity.senderId}\``);
|
|
72652
73266
|
}
|
|
@@ -72662,7 +73276,7 @@ function renderRouteStatusMessage(params) {
|
|
|
72662
73276
|
if (params.identity.topicId) {
|
|
72663
73277
|
lines.push(`topicId: \`${params.identity.topicId}\``);
|
|
72664
73278
|
}
|
|
72665
|
-
lines.push(`resumeCommand: \`${params.sessionDiagnostics.resumeCommand ?? "(not available yet)"}\``, `principal: \`${params.auth.principal ?? "(none)"}\``, `
|
|
73279
|
+
lines.push(`resumeCommand: \`${params.sessionDiagnostics.resumeCommand ?? "(not available yet)"}\``, `principal: \`${params.auth.principal ?? "(none)"}\``, `streaming: \`${params.route.streaming}\``, `response: \`${params.route.response}\``, `additionalMessageMode: \`${params.route.additionalMessageMode}\``, `surfaceNotifications.queueStart: \`${params.route.surfaceNotifications.queueStart}\``, `surfaceNotifications.loopStart: \`${params.route.surfaceNotifications.loopStart}\``, `verbose: \`${params.route.verbose}\``, `appRole: \`${params.auth.appRole}\``, `agentRole: \`${params.auth.agentRole}\``, `mayBypassSharedSenderPolicy: \`${params.auth.mayBypassSharedSenderPolicy}\``, `mayManageProtectedResources: \`${params.auth.mayManageProtectedResources}\``, `canUseShell: \`${params.auth.canUseShell}\``, `timezone.effective: \`${params.timezone.effective}\``, `timezone.route: \`${params.timezone.route ?? "(inherit)"}\``, `timezone.bot: \`${params.timezone.bot ?? "(inherit)"}\``, `followUp.mode: \`${params.followUpState.overrideMode ?? params.route.followUp.mode}\``, `followUp.windowMinutes: \`${formatFollowUpTtlMinutes(params.route.followUp.participationTtlMs)}\``, `run.state: \`${params.runtimeState.state}\``);
|
|
72666
73280
|
if (params.runtimeState.startedAt) {
|
|
72667
73281
|
lines.push(`run.startedAt: \`${new Date(params.runtimeState.startedAt).toISOString()}\``);
|
|
72668
73282
|
}
|
|
@@ -73422,7 +74036,6 @@ async function processChannelInteraction(params) {
|
|
|
73422
74036
|
};
|
|
73423
74037
|
const queueItem = forceQueuedDelivery ? createStoredQueueItem({
|
|
73424
74038
|
promptText: delayedPromptQueueText,
|
|
73425
|
-
canonicalPromptText: delayedPromptQueueText,
|
|
73426
74039
|
promptSummary: explicitQueueMessage ?? summarizeQueuePrompt(delayedPromptQueueText),
|
|
73427
74040
|
createdBy: params.senderId,
|
|
73428
74041
|
sender: buildLoopSender(params.identity),
|
|
@@ -73552,14 +74165,18 @@ async function processChannelInteraction(params) {
|
|
|
73552
74165
|
await params.agentService.recordConversationReply(params.sessionTarget);
|
|
73553
74166
|
return interactionResult;
|
|
73554
74167
|
}
|
|
73555
|
-
|
|
73556
|
-
|
|
73557
|
-
|
|
73558
|
-
|
|
73559
|
-
|
|
73560
|
-
|
|
73561
|
-
|
|
74168
|
+
try {
|
|
74169
|
+
const rotated = await params.agentService.triggerNewSession(params.sessionTarget);
|
|
74170
|
+
await params.postText([
|
|
74171
|
+
`Triggered a new runner conversation for agent \`${rotated.agentId}\`.`,
|
|
74172
|
+
`sessionName: \`${rotated.sessionName}\``,
|
|
74173
|
+
`sessionId: \`${rotated.sessionId ?? "none"}\``,
|
|
74174
|
+
rotated.restartedRunner ? "No live runner existed, so clisbot opened a fresh runner session." : `triggerCommand: \`${rotated.command}\``
|
|
74175
|
+
].join(`
|
|
73562
74176
|
`));
|
|
74177
|
+
} catch (error) {
|
|
74178
|
+
await params.postText(renderNewSessionFailureMessage(error));
|
|
74179
|
+
}
|
|
73563
74180
|
await params.agentService.recordConversationReply(params.sessionTarget);
|
|
73564
74181
|
return interactionResult;
|
|
73565
74182
|
}
|
|
@@ -73735,7 +74352,7 @@ async function processChannelInteraction(params) {
|
|
|
73735
74352
|
await params.agentService.recordConversationReply(params.sessionTarget);
|
|
73736
74353
|
return interactionResult;
|
|
73737
74354
|
}
|
|
73738
|
-
const cancelled = await params.agentService.cancelIntervalLoop(targetLoopId);
|
|
74355
|
+
const cancelled = await params.agentService.cancelIntervalLoop(params.sessionTarget, targetLoopId);
|
|
73739
74356
|
await params.postText(cancelled ? `Cancelled loop \`${targetLoopId}\`.` : `No active loop found with id \`${targetLoopId}\`.`);
|
|
73740
74357
|
await params.agentService.recordConversationReply(params.sessionTarget);
|
|
73741
74358
|
return interactionResult;
|
|
@@ -73820,9 +74437,9 @@ ${renderLoopUsage()}`);
|
|
|
73820
74437
|
const createdLoop2 = await params.agentService.createCalendarLoop({
|
|
73821
74438
|
target: params.sessionTarget,
|
|
73822
74439
|
promptText: resolvedLoopPrompt.text,
|
|
73823
|
-
canonicalPromptText: resolvedLoopPrompt.text,
|
|
73824
74440
|
promptSummary: summarizeLoopPrompt(resolvedLoopPrompt.text, resolvedLoopPrompt.maintenancePrompt),
|
|
73825
74441
|
promptSource: resolvedLoopPrompt.maintenancePrompt ? "LOOP.md" : "custom",
|
|
74442
|
+
loopStart: slashCommand.params.loopStart,
|
|
73826
74443
|
surfaceBinding: buildLoopSurfaceBinding(params.identity),
|
|
73827
74444
|
cadence: slashCommand.params.cadence,
|
|
73828
74445
|
dayOfWeek: slashCommand.params.dayOfWeek,
|
|
@@ -73859,9 +74476,9 @@ ${renderLoopUsage()}`);
|
|
|
73859
74476
|
const createdLoop = await params.agentService.createIntervalLoop({
|
|
73860
74477
|
target: params.sessionTarget,
|
|
73861
74478
|
promptText: resolvedLoopPrompt.text,
|
|
73862
|
-
canonicalPromptText: resolvedLoopPrompt.text,
|
|
73863
74479
|
promptSummary: summarizeLoopPrompt(resolvedLoopPrompt.text, resolvedLoopPrompt.maintenancePrompt),
|
|
73864
74480
|
promptSource: resolvedLoopPrompt.maintenancePrompt ? "LOOP.md" : "custom",
|
|
74481
|
+
loopStart: slashCommand.params.loopStart,
|
|
73865
74482
|
surfaceBinding: buildLoopSurfaceBinding(params.identity),
|
|
73866
74483
|
intervalMs: effectiveIntervalMs,
|
|
73867
74484
|
maxRuns: maxRunsPerLoop,
|
|
@@ -75347,9 +75964,9 @@ function getSlackMaxChars(maxMessageChars) {
|
|
|
75347
75964
|
}
|
|
75348
75965
|
|
|
75349
75966
|
// src/channels/runtime-identity.ts
|
|
75350
|
-
import { createHash } from "node:crypto";
|
|
75967
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
75351
75968
|
function buildTokenHint(token) {
|
|
75352
|
-
return
|
|
75969
|
+
return createHash2("sha256").update(token.trim()).digest("hex").slice(0, 8);
|
|
75353
75970
|
}
|
|
75354
75971
|
|
|
75355
75972
|
// src/channels/slack/service.ts
|
|
@@ -75609,6 +76226,12 @@ class SlackSocketService {
|
|
|
75609
76226
|
allowFrom: params.route.allowUsers ?? [],
|
|
75610
76227
|
userId: senderId
|
|
75611
76228
|
})) {
|
|
76229
|
+
const explicitlyAddressed = params.wasMentioned || hasBotMention(event.text ?? "", this.botUserId);
|
|
76230
|
+
if (params.route.requireMention && !explicitlyAddressed) {
|
|
76231
|
+
debugSlackEvent("drop-shared-not-addressed", { eventId, senderId });
|
|
76232
|
+
await this.processedEventsStore.markCompleted(eventId);
|
|
76233
|
+
return;
|
|
76234
|
+
}
|
|
75612
76235
|
try {
|
|
75613
76236
|
await postSlackText(this.app.client, {
|
|
75614
76237
|
channel: channelId,
|
|
@@ -75949,7 +76572,7 @@ class SlackSocketService {
|
|
|
75949
76572
|
senderId: slackSenderId,
|
|
75950
76573
|
text,
|
|
75951
76574
|
agentPromptText,
|
|
75952
|
-
agentPromptBuilder: (nextText) => buildAgentPromptText({
|
|
76575
|
+
agentPromptBuilder: (nextText, options) => buildAgentPromptText({
|
|
75953
76576
|
text: enrichPromptText(nextText),
|
|
75954
76577
|
identity,
|
|
75955
76578
|
config: this.getBotConfig().agentPrompt,
|
|
@@ -75967,7 +76590,8 @@ class SlackSocketService {
|
|
|
75967
76590
|
agentId: params.route.agentId,
|
|
75968
76591
|
routeTimezone: params.route.timezone,
|
|
75969
76592
|
botTimezone: params.route.botTimezone
|
|
75970
|
-
}).timezone
|
|
76593
|
+
}).timezone,
|
|
76594
|
+
maxProgressMessagesOverride: options?.maxProgressMessagesOverride
|
|
75971
76595
|
}),
|
|
75972
76596
|
promptContext,
|
|
75973
76597
|
protectedControlMutationRule,
|
|
@@ -76538,10 +77162,11 @@ function resolveSlackMessageContent(params) {
|
|
|
76538
77162
|
throw new Error("Slack blocks input supports only --render none or --render blocks");
|
|
76539
77163
|
}
|
|
76540
77164
|
const blocks = parseSlackBlocksInput(text);
|
|
77165
|
+
const fallbackText = buildSlackBlocksFallbackText(blocks);
|
|
76541
77166
|
return {
|
|
76542
|
-
text:
|
|
77167
|
+
text: fallbackText,
|
|
76543
77168
|
blocks,
|
|
76544
|
-
apiText:
|
|
77169
|
+
apiText: fallbackText
|
|
76545
77170
|
};
|
|
76546
77171
|
}
|
|
76547
77172
|
if (inputFormat === "html") {
|
|
@@ -76549,10 +77174,11 @@ function resolveSlackMessageContent(params) {
|
|
|
76549
77174
|
}
|
|
76550
77175
|
if (renderMode === "blocks") {
|
|
76551
77176
|
const blocks = renderMarkdownToSlackBlocks(text);
|
|
77177
|
+
const fallbackText = buildSlackBlocksFallbackText(blocks);
|
|
76552
77178
|
return {
|
|
76553
|
-
text:
|
|
77179
|
+
text: fallbackText,
|
|
76554
77180
|
blocks,
|
|
76555
|
-
apiText:
|
|
77181
|
+
apiText: fallbackText
|
|
76556
77182
|
};
|
|
76557
77183
|
}
|
|
76558
77184
|
if (renderMode === "html") {
|
|
@@ -77538,6 +78164,33 @@ function sanitizeTelegramHref(rawHref) {
|
|
|
77538
78164
|
}
|
|
77539
78165
|
return null;
|
|
77540
78166
|
}
|
|
78167
|
+
function splitTrailingUrlPunctuation(rawUrl) {
|
|
78168
|
+
let core = rawUrl;
|
|
78169
|
+
let trailing = "";
|
|
78170
|
+
while (/[.,!?;:]$/.test(core)) {
|
|
78171
|
+
trailing = core.slice(-1) + trailing;
|
|
78172
|
+
core = core.slice(0, -1);
|
|
78173
|
+
}
|
|
78174
|
+
while (core.endsWith(")")) {
|
|
78175
|
+
const openCount = (core.match(/\(/g) ?? []).length;
|
|
78176
|
+
const closeCount = (core.match(/\)/g) ?? []).length;
|
|
78177
|
+
if (closeCount <= openCount) {
|
|
78178
|
+
break;
|
|
78179
|
+
}
|
|
78180
|
+
trailing = ")" + trailing;
|
|
78181
|
+
core = core.slice(0, -1);
|
|
78182
|
+
}
|
|
78183
|
+
while (core.endsWith("]")) {
|
|
78184
|
+
const openCount = (core.match(/\[/g) ?? []).length;
|
|
78185
|
+
const closeCount = (core.match(/]/g) ?? []).length;
|
|
78186
|
+
if (closeCount <= openCount) {
|
|
78187
|
+
break;
|
|
78188
|
+
}
|
|
78189
|
+
trailing = "]" + trailing;
|
|
78190
|
+
core = core.slice(0, -1);
|
|
78191
|
+
}
|
|
78192
|
+
return { core, trailing };
|
|
78193
|
+
}
|
|
77541
78194
|
function storeToken(tokens, value) {
|
|
77542
78195
|
const token = `${TOKEN_PREFIX}${tokens.length};\x00`;
|
|
77543
78196
|
tokens.push(value);
|
|
@@ -77564,6 +78217,14 @@ function renderInlineMarkdownToTelegramHtml(text) {
|
|
|
77564
78217
|
}
|
|
77565
78218
|
return storeToken(tokens, `<a href="${safeHref}">${escapeHtml(label)}</a>`);
|
|
77566
78219
|
});
|
|
78220
|
+
working = working.replaceAll(/\b(?:https?:\/\/|tg:\/\/|mailto:)[^\s<>"`]+/g, (rawUrl) => {
|
|
78221
|
+
const { core, trailing } = splitTrailingUrlPunctuation(rawUrl);
|
|
78222
|
+
const safeHref = sanitizeTelegramHref(core);
|
|
78223
|
+
if (!safeHref) {
|
|
78224
|
+
return rawUrl;
|
|
78225
|
+
}
|
|
78226
|
+
return storeToken(tokens, `<a href="${safeHref}">${escapeHtml(core)}</a>`) + trailing;
|
|
78227
|
+
});
|
|
77567
78228
|
working = escapeHtml(working);
|
|
77568
78229
|
working = applyInlineFormatting(working);
|
|
77569
78230
|
return restoreTokens(working, tokens);
|
|
@@ -77955,6 +78616,7 @@ var TELEGRAM_FULL_COMMANDS = [
|
|
|
77955
78616
|
{ command: "detach", description: "Stop live updates for this thread" },
|
|
77956
78617
|
{ command: "watch", description: "Watch the active run on an interval" },
|
|
77957
78618
|
{ command: "stop", description: "Interrupt current run" },
|
|
78619
|
+
{ command: "new", description: "Start new session" },
|
|
77958
78620
|
{ command: "nudge", description: "Send one extra Enter to the session" },
|
|
77959
78621
|
{ command: "followup", description: "Show or change follow-up mode" },
|
|
77960
78622
|
{ command: "mention", description: "Require explicit mention for later turns" },
|
|
@@ -78580,7 +79242,7 @@ class TelegramPollingService {
|
|
|
78580
79242
|
senderId: message.from?.id != null ? String(message.from.id).trim() : undefined,
|
|
78581
79243
|
text,
|
|
78582
79244
|
agentPromptText,
|
|
78583
|
-
agentPromptBuilder: (nextText) => buildAgentPromptText({
|
|
79245
|
+
agentPromptBuilder: (nextText, options) => buildAgentPromptText({
|
|
78584
79246
|
text: enrichPromptText(nextText),
|
|
78585
79247
|
identity,
|
|
78586
79248
|
config: this.getBotConfig().agentPrompt,
|
|
@@ -78598,7 +79260,8 @@ class TelegramPollingService {
|
|
|
78598
79260
|
agentId: route.agentId,
|
|
78599
79261
|
routeTimezone: route.timezone,
|
|
78600
79262
|
botTimezone: route.botTimezone
|
|
78601
|
-
}).timezone
|
|
79263
|
+
}).timezone,
|
|
79264
|
+
maxProgressMessagesOverride: options?.maxProgressMessagesOverride
|
|
78602
79265
|
}),
|
|
78603
79266
|
promptContext,
|
|
78604
79267
|
protectedControlMutationRule,
|
|
@@ -79286,8 +79949,8 @@ var defaultRuntimeMonitorDependencies = {
|
|
|
79286
79949
|
},
|
|
79287
79950
|
readState: readRuntimeMonitorState,
|
|
79288
79951
|
writeState: writeRuntimeMonitorState,
|
|
79289
|
-
removePid: (pidPath) =>
|
|
79290
|
-
removeRuntimeCredentials: (runtimeCredentialsPath) =>
|
|
79952
|
+
removePid: (pidPath) => rmSync3(pidPath, { force: true }),
|
|
79953
|
+
removeRuntimeCredentials: (runtimeCredentialsPath) => rmSync3(runtimeCredentialsPath, { force: true }),
|
|
79291
79954
|
sleep,
|
|
79292
79955
|
now: () => Date.now(),
|
|
79293
79956
|
spawnChild: (command, args, options) => spawn2(command, args, {
|
|
@@ -79837,7 +80500,7 @@ async function startDetachedRuntime(params) {
|
|
|
79837
80500
|
};
|
|
79838
80501
|
}
|
|
79839
80502
|
if (existingPid) {
|
|
79840
|
-
|
|
80503
|
+
rmSync4(pidPath, { force: true });
|
|
79841
80504
|
}
|
|
79842
80505
|
if (existingMonitorState?.runtimePid && isProcessRunning(existingMonitorState.runtimePid)) {
|
|
79843
80506
|
kill2(existingMonitorState.runtimePid, "SIGTERM");
|
|
@@ -79946,7 +80609,7 @@ async function stopDetachedRuntime(params, dependencies = {}) {
|
|
|
79946
80609
|
}
|
|
79947
80610
|
}
|
|
79948
80611
|
}
|
|
79949
|
-
|
|
80612
|
+
rmSync4(pidPath, { force: true });
|
|
79950
80613
|
removeRuntimeCredentials(runtimeCredentialsPath);
|
|
79951
80614
|
await disableExpiredMemAccountsInConfig(configPath);
|
|
79952
80615
|
if (monitorState) {
|
|
@@ -79988,7 +80651,7 @@ async function writeRuntimePid(pidPath, pid = process.pid) {
|
|
|
79988
80651
|
`);
|
|
79989
80652
|
}
|
|
79990
80653
|
function removeRuntimePid(pidPath) {
|
|
79991
|
-
|
|
80654
|
+
rmSync4(resolvePidPath(pidPath), { force: true });
|
|
79992
80655
|
}
|
|
79993
80656
|
async function getRuntimeStatus(params = {}) {
|
|
79994
80657
|
const configPath = resolveConfigPath2(params.configPath);
|
|
@@ -80849,7 +81512,7 @@ function resolveSlackLoopCliContext(params) {
|
|
|
80849
81512
|
sessionTarget,
|
|
80850
81513
|
identity,
|
|
80851
81514
|
route,
|
|
80852
|
-
buildLoopPromptText: (text) => buildAgentPromptText({
|
|
81515
|
+
buildLoopPromptText: (text, options) => buildAgentPromptText({
|
|
80853
81516
|
text,
|
|
80854
81517
|
identity,
|
|
80855
81518
|
config: botConfig.agentPrompt,
|
|
@@ -80863,7 +81526,8 @@ function resolveSlackLoopCliContext(params) {
|
|
|
80863
81526
|
agentId: sessionTarget.agentId,
|
|
80864
81527
|
routeTimezone: route.timezone,
|
|
80865
81528
|
botTimezone: route.botTimezone
|
|
80866
|
-
}).timezone
|
|
81529
|
+
}).timezone,
|
|
81530
|
+
maxProgressMessagesOverride: options?.maxProgressMessagesOverride
|
|
80867
81531
|
})
|
|
80868
81532
|
};
|
|
80869
81533
|
}
|
|
@@ -80919,7 +81583,7 @@ function resolveTelegramLoopCliContext(params) {
|
|
|
80919
81583
|
sessionTarget,
|
|
80920
81584
|
identity,
|
|
80921
81585
|
route,
|
|
80922
|
-
buildLoopPromptText: (text) => buildAgentPromptText({
|
|
81586
|
+
buildLoopPromptText: (text, options) => buildAgentPromptText({
|
|
80923
81587
|
text,
|
|
80924
81588
|
identity,
|
|
80925
81589
|
config: botConfig.agentPrompt,
|
|
@@ -80933,7 +81597,8 @@ function resolveTelegramLoopCliContext(params) {
|
|
|
80933
81597
|
agentId: sessionTarget.agentId,
|
|
80934
81598
|
routeTimezone: route.timezone,
|
|
80935
81599
|
botTimezone: route.botTimezone
|
|
80936
|
-
}).timezone
|
|
81600
|
+
}).timezone,
|
|
81601
|
+
maxProgressMessagesOverride: options?.maxProgressMessagesOverride
|
|
80937
81602
|
})
|
|
80938
81603
|
};
|
|
80939
81604
|
}
|
|
@@ -81093,6 +81758,8 @@ function renderLoopsHelp() {
|
|
|
81093
81758
|
" - `--sender <principal>` is required when creating loops, using `slack:<user-id>` or `telegram:<user-id>`",
|
|
81094
81759
|
" - optional creator display fields: `--sender-name <name>` and `--sender-handle <handle>`",
|
|
81095
81760
|
" - `--timezone <iana>` is a one-off wall-clock loop override and is frozen on the created loop record",
|
|
81761
|
+
` - \`${LOOP_START_FLAG} <none|brief|full>\` controls scheduled loop-start notifications only; it does not control injected agent progress messages`,
|
|
81762
|
+
" - `--progress <count>` overrides loop progress-message injection for agent replies; `0` disables progress messages, and omitting the flag inherits the normal clisbot prompt config",
|
|
81096
81763
|
" - in Telegram forum groups, omitting `--topic-id` targets the parent chat surface; sends then follow Telegram's normal no-`message_thread_id` behavior, which is the General topic when that forum has one",
|
|
81097
81764
|
"",
|
|
81098
81765
|
"Expressions:",
|
|
@@ -81114,6 +81781,7 @@ function renderLoopsHelp() {
|
|
|
81114
81781
|
" - CLI loop creation fails without `--sender` so scheduled prompts can preserve creator identity",
|
|
81115
81782
|
" - the first wall-clock loop returns `confirmation_required` and does not persist until rerun with `--confirm`",
|
|
81116
81783
|
" - recurring interval loops and confirmed wall-clock loops are persisted immediately and picked up by the runtime when it is running",
|
|
81784
|
+
" - loop-created agent prompts inherit the normal clisbot prompt config unless `--progress <count>` overrides that loop",
|
|
81117
81785
|
" - if runtime is stopped, recurring loops activate on the next `clisbot start`",
|
|
81118
81786
|
" - global `cancel --all` clears the whole app; scoped `cancel --all` clears one routed session",
|
|
81119
81787
|
" - `cancel --all --app` is accepted only with a scoped session target, matching `/loop cancel --all --app`",
|
|
@@ -81143,6 +81811,8 @@ function renderLoopsCreateHelp() {
|
|
|
81143
81811
|
" - `--new-thread` creates a Slack thread anchor before persisting the loop",
|
|
81144
81812
|
" - `--timezone <iana>` freezes a one-off wall-clock timezone on the loop record",
|
|
81145
81813
|
" - `--confirm` persists the first wall-clock loop after reviewing the confirmation output",
|
|
81814
|
+
` - advanced: \`${LOOP_START_FLAG} <none|brief|full>\` overrides the default scheduled loop-start notification behavior for that recurring loop`,
|
|
81815
|
+
" - advanced: `--progress <count>` overrides loop agent progress-message injection; `0` disables progress messages, and omitting the flag inherits the normal clisbot prompt config",
|
|
81146
81816
|
"",
|
|
81147
81817
|
"Examples:",
|
|
81148
81818
|
` ${renderCliCommand("loops create --channel slack --target group:C1234567890 --thread-id 1712345678.123456 --sender slack:U1234567890 every day at 07:00 check CI")}`,
|
|
@@ -81152,7 +81822,8 @@ function renderLoopsCreateHelp() {
|
|
|
81152
81822
|
"Behavior:",
|
|
81153
81823
|
" - create without `--sender` fails by design",
|
|
81154
81824
|
" - the `--sender` platform must match `--channel`",
|
|
81155
|
-
" - recurring CLI-created loops persist creator metadata into the session store"
|
|
81825
|
+
" - recurring CLI-created loops persist creator metadata into the session store",
|
|
81826
|
+
" - CLI-created loop prompts inherit the normal clisbot prompt config unless `--progress <count>` is provided"
|
|
81156
81827
|
].join(`
|
|
81157
81828
|
`);
|
|
81158
81829
|
}
|
|
@@ -81363,6 +82034,7 @@ async function getScopedLoopCounts(params) {
|
|
|
81363
82034
|
// src/control/loops-cli.ts
|
|
81364
82035
|
var LOOP_BUSY_RETRY_MS = 250;
|
|
81365
82036
|
var LOOP_CONFIRM_FLAG = "--confirm";
|
|
82037
|
+
var LOOP_PROGRESS_FLAG = "--progress";
|
|
81366
82038
|
var LOOP_SENDER_FLAG = "--sender";
|
|
81367
82039
|
var LOOP_SENDER_NAME_FLAG = "--sender-name";
|
|
81368
82040
|
var LOOP_SENDER_HANDLE_FLAG = "--sender-handle";
|
|
@@ -81522,7 +82194,9 @@ async function waitForSessionIdle(agentService, target) {
|
|
|
81522
82194
|
}
|
|
81523
82195
|
async function executeCountLoop(params) {
|
|
81524
82196
|
const agentService = new AgentService(params.state.loadedConfig);
|
|
81525
|
-
const builtPrompt = params.context.buildLoopPromptText(params.promptText
|
|
82197
|
+
const builtPrompt = params.context.buildLoopPromptText(params.promptText, params.progressMessages == null ? undefined : {
|
|
82198
|
+
maxProgressMessagesOverride: params.progressMessages
|
|
82199
|
+
});
|
|
81526
82200
|
console.log(renderLoopStartedMessage({
|
|
81527
82201
|
mode: "times",
|
|
81528
82202
|
count: params.count,
|
|
@@ -81576,8 +82250,35 @@ function stripLoopCreatorArgs(args) {
|
|
|
81576
82250
|
}
|
|
81577
82251
|
return remaining;
|
|
81578
82252
|
}
|
|
82253
|
+
function parseLoopProgress(args) {
|
|
82254
|
+
const raw = parseOptionValue3(args, LOOP_PROGRESS_FLAG);
|
|
82255
|
+
if (raw == null) {
|
|
82256
|
+
return;
|
|
82257
|
+
}
|
|
82258
|
+
const parsed = Number.parseInt(raw, 10);
|
|
82259
|
+
if (!Number.isFinite(parsed) || String(parsed) !== raw.trim() || parsed < 0) {
|
|
82260
|
+
throw new Error(`${LOOP_PROGRESS_FLAG} must be a non-negative integer.`);
|
|
82261
|
+
}
|
|
82262
|
+
return parsed;
|
|
82263
|
+
}
|
|
82264
|
+
function stripLoopProgressArgs(args) {
|
|
82265
|
+
const remaining = [];
|
|
82266
|
+
for (let index = 0;index < args.length; index += 1) {
|
|
82267
|
+
const current = args[index];
|
|
82268
|
+
if (current === "--") {
|
|
82269
|
+
remaining.push(...args.slice(index));
|
|
82270
|
+
break;
|
|
82271
|
+
}
|
|
82272
|
+
if (current === LOOP_PROGRESS_FLAG) {
|
|
82273
|
+
index += 1;
|
|
82274
|
+
continue;
|
|
82275
|
+
}
|
|
82276
|
+
remaining.push(current);
|
|
82277
|
+
}
|
|
82278
|
+
return remaining;
|
|
82279
|
+
}
|
|
81579
82280
|
function parseCreateExpression(rawArgs, explicitCreateSubcommand) {
|
|
81580
|
-
const expressionArgs = stripLoopContextArgs(stripLoopCreatorArgs(stripConfirmFlag(explicitCreateSubcommand ? rawArgs.slice(1) : rawArgs)));
|
|
82281
|
+
const expressionArgs = stripLoopContextArgs(stripLoopCreatorArgs(stripLoopProgressArgs(stripConfirmFlag(explicitCreateSubcommand ? rawArgs.slice(1) : rawArgs))));
|
|
81581
82282
|
const expression = expressionArgs.join(" ").trim();
|
|
81582
82283
|
if (!expression) {
|
|
81583
82284
|
throw new Error("Loop creation requires an interval, count, or schedule expression.");
|
|
@@ -81657,6 +82358,7 @@ function requireValidIntervalLoop(parsed) {
|
|
|
81657
82358
|
async function resolveLoopCreateRequest(state, rawArgs, explicitCreateSubcommand) {
|
|
81658
82359
|
const confirm = hasFlag4(rawArgs, LOOP_CONFIRM_FLAG);
|
|
81659
82360
|
const loopTimezone = parseLoopTimezone(rawArgs);
|
|
82361
|
+
const progressMessages = parseLoopProgress(rawArgs);
|
|
81660
82362
|
const expression = parseCreateExpression(rawArgs, explicitCreateSubcommand);
|
|
81661
82363
|
const parsed = parseCreateCommand(expression);
|
|
81662
82364
|
let addressing = parseAddressing(rawArgs);
|
|
@@ -81686,7 +82388,8 @@ async function resolveLoopCreateRequest(state, rawArgs, explicitCreateSubcommand
|
|
|
81686
82388
|
maxActiveLoops,
|
|
81687
82389
|
expression,
|
|
81688
82390
|
confirm,
|
|
81689
|
-
loopTimezone
|
|
82391
|
+
loopTimezone,
|
|
82392
|
+
progressMessages
|
|
81690
82393
|
};
|
|
81691
82394
|
}
|
|
81692
82395
|
addressing = await prepareLoopCreateAddressing({
|
|
@@ -81717,7 +82420,8 @@ async function resolveLoopCreateRequest(state, rawArgs, explicitCreateSubcommand
|
|
|
81717
82420
|
maxActiveLoops,
|
|
81718
82421
|
expression,
|
|
81719
82422
|
confirm,
|
|
81720
|
-
loopTimezone
|
|
82423
|
+
loopTimezone,
|
|
82424
|
+
progressMessages
|
|
81721
82425
|
};
|
|
81722
82426
|
}
|
|
81723
82427
|
function buildLoopSurfaceBinding2(request) {
|
|
@@ -81746,9 +82450,10 @@ function buildRecurringLoopCreateBase(state, request) {
|
|
|
81746
82450
|
function buildRecurringLoopPromptMetadata(request) {
|
|
81747
82451
|
return {
|
|
81748
82452
|
promptText: request.resolvedPrompt.text,
|
|
81749
|
-
canonicalPromptText: request.resolvedPrompt.text,
|
|
81750
82453
|
promptSummary: summarizeLoopPrompt(request.resolvedPrompt.text, request.resolvedPrompt.maintenancePrompt),
|
|
81751
82454
|
promptSource: request.resolvedPrompt.maintenancePrompt ? "LOOP.md" : "custom",
|
|
82455
|
+
progressMessages: request.progressMessages,
|
|
82456
|
+
loopStart: request.parsed.mode === "times" ? undefined : request.parsed.loopStart,
|
|
81752
82457
|
maintenancePrompt: request.resolvedPrompt.maintenancePrompt,
|
|
81753
82458
|
createdBy: request.creator.providerId,
|
|
81754
82459
|
sender: request.creator,
|
|
@@ -81888,7 +82593,8 @@ async function createLoop(state, rawArgs, options = {}) {
|
|
|
81888
82593
|
context: request.deliveryContext ?? request.context,
|
|
81889
82594
|
promptText: request.resolvedPrompt.text,
|
|
81890
82595
|
count: request.parsed.count,
|
|
81891
|
-
maintenancePrompt: request.resolvedPrompt.maintenancePrompt
|
|
82596
|
+
maintenancePrompt: request.resolvedPrompt.maintenancePrompt,
|
|
82597
|
+
progressMessages: request.progressMessages
|
|
81892
82598
|
});
|
|
81893
82599
|
return;
|
|
81894
82600
|
}
|
|
@@ -82254,6 +82960,11 @@ async function runMessageCli(args, dependencies = defaultMessageCliDependencies)
|
|
|
82254
82960
|
var QUEUE_SENDER_FLAG = "--sender";
|
|
82255
82961
|
var QUEUE_SENDER_NAME_FLAG = "--sender-name";
|
|
82256
82962
|
var QUEUE_SENDER_HANDLE_FLAG = "--sender-handle";
|
|
82963
|
+
var defaultQueueCliDependencies = {
|
|
82964
|
+
print: (text) => console.log(text),
|
|
82965
|
+
warn: (text) => console.warn(text),
|
|
82966
|
+
sendQueueCreatedNotification: sendQueueCreatedNotificationToSurface
|
|
82967
|
+
};
|
|
82257
82968
|
function getEditableConfigPath9() {
|
|
82258
82969
|
return process.env.CLISBOT_CONFIG_PATH;
|
|
82259
82970
|
}
|
|
@@ -82411,7 +83122,6 @@ function resolveProtectedControlMutationRule(state, agentId, sender) {
|
|
|
82411
83122
|
function createQueueItemForContext(params) {
|
|
82412
83123
|
return createStoredQueueItem({
|
|
82413
83124
|
promptText: params.promptText,
|
|
82414
|
-
canonicalPromptText: params.promptText,
|
|
82415
83125
|
protectedControlMutationRule: resolveProtectedControlMutationRule(params.state, params.context.sessionTarget.agentId, params.sender),
|
|
82416
83126
|
promptSummary: params.promptText,
|
|
82417
83127
|
createdBy: params.sender.providerId,
|
|
@@ -82419,6 +83129,56 @@ function createQueueItemForContext(params) {
|
|
|
82419
83129
|
surfaceBinding: buildQueueSurfaceBinding(params.context)
|
|
82420
83130
|
});
|
|
82421
83131
|
}
|
|
83132
|
+
function renderQueueCreatedNotification(params) {
|
|
83133
|
+
const queueLine = params.positionAhead > 0 ? `Queued \`${params.queueId}\`: ${params.positionAhead} ahead.` : `Queued \`${params.queueId}\`.`;
|
|
83134
|
+
return `${queueLine}
|
|
83135
|
+
|
|
83136
|
+
${params.promptText.trim()}`;
|
|
83137
|
+
}
|
|
83138
|
+
async function getQueuePositionAhead(state, sessionKey, itemId) {
|
|
83139
|
+
const queues = await state.sessionState.listQueuedItems({
|
|
83140
|
+
sessionKey,
|
|
83141
|
+
statuses: ["pending", "running"]
|
|
83142
|
+
});
|
|
83143
|
+
const index = queues.findIndex((item) => item.id === itemId);
|
|
83144
|
+
return index >= 0 ? index : 0;
|
|
83145
|
+
}
|
|
83146
|
+
function buildQueueCreatedMessageCommand(params) {
|
|
83147
|
+
return {
|
|
83148
|
+
action: "send",
|
|
83149
|
+
channel: params.context.channel,
|
|
83150
|
+
account: params.context.botId,
|
|
83151
|
+
target: params.context.target,
|
|
83152
|
+
message: params.text,
|
|
83153
|
+
threadId: params.context.threadId,
|
|
83154
|
+
remove: false,
|
|
83155
|
+
pollOptions: [],
|
|
83156
|
+
forceDocument: false,
|
|
83157
|
+
silent: false,
|
|
83158
|
+
progress: false,
|
|
83159
|
+
final: false,
|
|
83160
|
+
json: false,
|
|
83161
|
+
inputFormat: "plain",
|
|
83162
|
+
renderMode: "none"
|
|
83163
|
+
};
|
|
83164
|
+
}
|
|
83165
|
+
async function sendQueueCreatedNotificationToSurface(params) {
|
|
83166
|
+
if (!params.item.surfaceBinding) {
|
|
83167
|
+
return;
|
|
83168
|
+
}
|
|
83169
|
+
const loadedConfig = await loadConfig(params.state.configPath, {
|
|
83170
|
+
materializeChannels: [params.context.channel]
|
|
83171
|
+
});
|
|
83172
|
+
const plugin = listChannelPlugins().find((entry) => entry.id === params.context.channel);
|
|
83173
|
+
if (!plugin) {
|
|
83174
|
+
throw new Error(`Unsupported queue notification channel: ${params.context.channel}`);
|
|
83175
|
+
}
|
|
83176
|
+
await plugin.runMessageCommand(loadedConfig, buildQueueCreatedMessageCommand({
|
|
83177
|
+
context: params.context,
|
|
83178
|
+
text: params.text
|
|
83179
|
+
}));
|
|
83180
|
+
await params.state.sessionState.recordConversationReply(params.resolved);
|
|
83181
|
+
}
|
|
82422
83182
|
function renderQueueInventory(params) {
|
|
82423
83183
|
const lines = [
|
|
82424
83184
|
`Queue ${params.commandLabel}`,
|
|
@@ -82433,20 +83193,20 @@ function renderQueueInventory(params) {
|
|
|
82433
83193
|
return lines.join(`
|
|
82434
83194
|
`);
|
|
82435
83195
|
}
|
|
82436
|
-
async function listQueues(state, addressing, commandLabel) {
|
|
83196
|
+
async function listQueues(state, addressing, commandLabel, deps) {
|
|
82437
83197
|
const context = addressing.channel || addressing.target ? resolveScopedContext(state, addressing) : undefined;
|
|
82438
83198
|
const sessionKey = context?.sessionTarget.sessionKey;
|
|
82439
83199
|
const queues = await state.sessionState.listQueuedItems({
|
|
82440
83200
|
sessionKey,
|
|
82441
83201
|
statuses: commandLabel === "list" ? ["pending"] : ["pending", "running"]
|
|
82442
83202
|
});
|
|
82443
|
-
|
|
83203
|
+
deps.print(renderQueueInventory({
|
|
82444
83204
|
commandLabel,
|
|
82445
83205
|
sessionStorePath: state.sessionStorePath,
|
|
82446
83206
|
queues
|
|
82447
83207
|
}));
|
|
82448
83208
|
}
|
|
82449
|
-
async function createQueue(state, args) {
|
|
83209
|
+
async function createQueue(state, args, deps) {
|
|
82450
83210
|
const addressing = parseQueueCliAddressing(args);
|
|
82451
83211
|
const promptText = stripQueueArgs(args.slice(1)).join(" ").trim();
|
|
82452
83212
|
if (!promptText) {
|
|
@@ -82467,18 +83227,30 @@ async function createQueue(state, args) {
|
|
|
82467
83227
|
sender
|
|
82468
83228
|
});
|
|
82469
83229
|
await state.sessionState.setQueuedItem(resolved, item);
|
|
82470
|
-
|
|
83230
|
+
const positionAhead = await getQueuePositionAhead(state, context.sessionTarget.sessionKey, item.id);
|
|
83231
|
+
const text = renderQueueCreatedNotification({ queueId: item.id, positionAhead, promptText });
|
|
83232
|
+
await deps.sendQueueCreatedNotification({
|
|
83233
|
+
state,
|
|
83234
|
+
context,
|
|
83235
|
+
resolved,
|
|
83236
|
+
item,
|
|
83237
|
+
positionAhead,
|
|
83238
|
+
text
|
|
83239
|
+
}).catch((error) => {
|
|
83240
|
+
deps.warn(`Queued prompt ${item.id}, but surface acknowledgement failed: ${String(error)}`);
|
|
83241
|
+
});
|
|
83242
|
+
deps.print(`Queued prompt \`${item.id}\` for \`${context.sessionTarget.sessionKey}\`.`);
|
|
82471
83243
|
}
|
|
82472
|
-
async function clearQueues(state, addressing) {
|
|
83244
|
+
async function clearQueues(state, addressing, deps) {
|
|
82473
83245
|
if (addressing.all) {
|
|
82474
83246
|
const cleared2 = await state.sessionState.clearAllPendingQueuedItems();
|
|
82475
|
-
|
|
83247
|
+
deps.print(`Cleared ${cleared2.length} pending queued prompt${cleared2.length === 1 ? "" : "s"} across the whole app.`);
|
|
82476
83248
|
return;
|
|
82477
83249
|
}
|
|
82478
83250
|
const context = resolveScopedContext(state, addressing);
|
|
82479
83251
|
const sessionKey = context.sessionTarget.sessionKey;
|
|
82480
83252
|
const cleared = await state.sessionState.clearPendingQueuedItemsForSessionKey(sessionKey);
|
|
82481
|
-
|
|
83253
|
+
deps.print(`Cleared ${cleared.length} pending queued prompt${cleared.length === 1 ? "" : "s"} for \`${sessionKey}\`.`);
|
|
82482
83254
|
}
|
|
82483
83255
|
function renderQueuesHelp() {
|
|
82484
83256
|
return [
|
|
@@ -82499,23 +83271,24 @@ function renderQueuesHelp() {
|
|
|
82499
83271
|
].join(`
|
|
82500
83272
|
`);
|
|
82501
83273
|
}
|
|
82502
|
-
async function runQueuesCli(args) {
|
|
83274
|
+
async function runQueuesCli(args, dependencies = {}) {
|
|
83275
|
+
const deps = { ...defaultQueueCliDependencies, ...dependencies };
|
|
82503
83276
|
if (args[0] === "--help" || args[0] === "help" || args.length === 0) {
|
|
82504
|
-
|
|
83277
|
+
deps.print(renderQueuesHelp());
|
|
82505
83278
|
return;
|
|
82506
83279
|
}
|
|
82507
83280
|
const command = args[0];
|
|
82508
83281
|
const state = await loadQueueControlState();
|
|
82509
83282
|
if (command === "list" || command === "status") {
|
|
82510
|
-
await listQueues(state, parseQueueCliAddressing(args.slice(1)), command);
|
|
83283
|
+
await listQueues(state, parseQueueCliAddressing(args.slice(1)), command, deps);
|
|
82511
83284
|
return;
|
|
82512
83285
|
}
|
|
82513
83286
|
if (command === "create") {
|
|
82514
|
-
await createQueue(state, args);
|
|
83287
|
+
await createQueue(state, args, deps);
|
|
82515
83288
|
return;
|
|
82516
83289
|
}
|
|
82517
83290
|
if (command === "clear") {
|
|
82518
|
-
await clearQueues(state, parseQueueCliAddressing(args.slice(1)));
|
|
83291
|
+
await clearQueues(state, parseQueueCliAddressing(args.slice(1)), deps);
|
|
82519
83292
|
return;
|
|
82520
83293
|
}
|
|
82521
83294
|
throw new Error(`Unknown queues subcommand: ${command}`);
|
|
@@ -83280,13 +84053,37 @@ function sortRunnerSessionMetadataNewestFirst(entries) {
|
|
|
83280
84053
|
return left.sessionName.localeCompare(right.sessionName);
|
|
83281
84054
|
});
|
|
83282
84055
|
}
|
|
84056
|
+
function deriveRunnerSessionIdentity(params) {
|
|
84057
|
+
const storedSessionId = params.entry?.sessionId?.trim() || undefined;
|
|
84058
|
+
const liveSessionId = params.liveSessionId?.trim() || undefined;
|
|
84059
|
+
const sessionId = liveSessionId ?? storedSessionId;
|
|
84060
|
+
return {
|
|
84061
|
+
sessionId,
|
|
84062
|
+
sessionIdPersistence: sessionId && sessionId === storedSessionId ? "persisted" : sessionId ? "not-persisted-yet" : undefined,
|
|
84063
|
+
storedSessionId
|
|
84064
|
+
};
|
|
84065
|
+
}
|
|
84066
|
+
function parseRunnerSessionIdFromSnapshot(loadedConfig, entry, snapshot) {
|
|
84067
|
+
if (!entry) {
|
|
84068
|
+
return;
|
|
84069
|
+
}
|
|
84070
|
+
const resolved = resolveAgentTarget(loadedConfig, {
|
|
84071
|
+
agentId: entry.agentId,
|
|
84072
|
+
sessionKey: entry.sessionKey
|
|
84073
|
+
});
|
|
84074
|
+
const pattern = resolved.runner.sessionId.capture.pattern?.trim();
|
|
84075
|
+
if (!pattern) {
|
|
84076
|
+
return;
|
|
84077
|
+
}
|
|
84078
|
+
return parseRunnerSessionId(snapshot, pattern) ?? undefined;
|
|
84079
|
+
}
|
|
83283
84080
|
async function listRunnerSessions(loadedConfig) {
|
|
83284
84081
|
const sessionStore = new SessionStore(resolveSessionStorePath(loadedConfig));
|
|
83285
84082
|
const metadata = buildRunnerSessionMetadata(loadedConfig, await sessionStore.list());
|
|
83286
84083
|
const sessionByName = new Map(metadata.map((item) => [item.sessionName, item.entry]));
|
|
83287
84084
|
const tmux = new TmuxClient(loadedConfig.raw.tmux.socketPath);
|
|
83288
84085
|
const liveSessionNames = new Set(await tmux.listSessions());
|
|
83289
|
-
|
|
84086
|
+
const orderedSessionNames = [...liveSessionNames].sort((left, right) => {
|
|
83290
84087
|
const leftEntry = sessionByName.get(left);
|
|
83291
84088
|
const rightEntry = sessionByName.get(right);
|
|
83292
84089
|
const leftPromptAt = leftEntry?.lastAdmittedPromptAt ?? 0;
|
|
@@ -83300,12 +84097,19 @@ async function listRunnerSessions(loadedConfig) {
|
|
|
83300
84097
|
return rightUpdatedAt - leftUpdatedAt;
|
|
83301
84098
|
}
|
|
83302
84099
|
return left.localeCompare(right);
|
|
83303
|
-
})
|
|
83304
|
-
|
|
83305
|
-
sessionName
|
|
83306
|
-
|
|
83307
|
-
|
|
83308
|
-
|
|
84100
|
+
});
|
|
84101
|
+
return orderedSessionNames.map((sessionName, index) => {
|
|
84102
|
+
const entry = sessionByName.get(sessionName);
|
|
84103
|
+
return {
|
|
84104
|
+
index: index + 1,
|
|
84105
|
+
sessionName,
|
|
84106
|
+
live: true,
|
|
84107
|
+
entry,
|
|
84108
|
+
identity: deriveRunnerSessionIdentity({
|
|
84109
|
+
entry
|
|
84110
|
+
})
|
|
84111
|
+
};
|
|
84112
|
+
});
|
|
83309
84113
|
}
|
|
83310
84114
|
|
|
83311
84115
|
// src/control/runner-cli.ts
|
|
@@ -83318,6 +84122,7 @@ var SMOKE_SCENARIOS = [
|
|
|
83318
84122
|
"recover_after_runner_loss"
|
|
83319
84123
|
];
|
|
83320
84124
|
var SMOKE_SUITES = ["launch-trio"];
|
|
84125
|
+
var MISSING_STORED_SESSION_ID_TEXT = "not stored";
|
|
83321
84126
|
function parseRepeatedOption4(args, name) {
|
|
83322
84127
|
const values = [];
|
|
83323
84128
|
for (let index = 0;index < args.length; index += 1) {
|
|
@@ -83428,7 +84233,7 @@ function renderRunnerHelp() {
|
|
|
83428
84233
|
` ${renderCliCommand("runner smoke --backend all --suite launch-trio [--workspace <path>] [--agent <id>] [--artifact-dir <path>] [--timeout-ms <n>] [--keep-session] [--json]")}`,
|
|
83429
84234
|
"",
|
|
83430
84235
|
"Operator session debugging:",
|
|
83431
|
-
" - `list` shows current tmux runner sessions, newest admitted turn first when known, plus
|
|
84236
|
+
" - `list` shows current tmux runner sessions, newest admitted turn first when known, plus sessionId and persistence state when available",
|
|
83432
84237
|
" - `inspect` captures one snapshot; default tail is 100 lines",
|
|
83433
84238
|
" - `--index <n>` selects the 1-based order printed by `runner list`",
|
|
83434
84239
|
" - `watch --latest` follows the session that most recently admitted a new prompt",
|
|
@@ -83604,10 +84409,11 @@ function renderWatchFrame(params) {
|
|
|
83604
84409
|
"",
|
|
83605
84410
|
`session: ${params.sessionName}`,
|
|
83606
84411
|
params.agentId ? `agent: ${params.agentId}` : null,
|
|
83607
|
-
|
|
84412
|
+
`sessionId: ${params.sessionId?.trim() || MISSING_STORED_SESSION_ID_TEXT}`,
|
|
84413
|
+
`sessionIdPersistence: ${params.sessionIdPersistence ?? "not stored yet"}`,
|
|
84414
|
+
params.storedSessionId && params.storedSessionId !== params.sessionId ? `storedSessionId: ${params.storedSessionId}` : null,
|
|
83608
84415
|
`lines: ${params.lines}`,
|
|
83609
|
-
`
|
|
83610
|
-
`status: ${params.status}`,
|
|
84416
|
+
`state: ${params.state}`,
|
|
83611
84417
|
"",
|
|
83612
84418
|
params.snapshot.trimEnd() || "(empty pane)"
|
|
83613
84419
|
].filter((line) => Boolean(line)).join(`
|
|
@@ -83644,21 +84450,20 @@ function renderRunnerListSession(session) {
|
|
|
83644
84450
|
if (!session.entry) {
|
|
83645
84451
|
return [
|
|
83646
84452
|
prefix,
|
|
83647
|
-
|
|
83648
|
-
" state: unmanaged"
|
|
83649
|
-
" live: yes"
|
|
84453
|
+
` sessionId: ${MISSING_STORED_SESSION_ID_TEXT}`,
|
|
84454
|
+
" state: unmanaged"
|
|
83650
84455
|
].join(`
|
|
83651
84456
|
`);
|
|
83652
84457
|
}
|
|
83653
84458
|
return [
|
|
83654
84459
|
prefix,
|
|
83655
84460
|
` agent: ${session.entry.agentId}`,
|
|
83656
|
-
`
|
|
83657
|
-
`
|
|
84461
|
+
` sessionId: ${session.identity?.sessionId?.trim() || MISSING_STORED_SESSION_ID_TEXT}`,
|
|
84462
|
+
` sessionIdPersistence: ${session.identity?.sessionIdPersistence ?? "not stored yet"}`,
|
|
84463
|
+
session.identity?.storedSessionId && session.identity.storedSessionId !== session.identity.sessionId ? ` storedSessionId: ${session.identity.storedSessionId}` : null,
|
|
83658
84464
|
` state: ${session.entry.runtime?.state ?? "no-runtime"}`,
|
|
83659
|
-
` live: ${session.live ? "yes" : "no"}`,
|
|
83660
84465
|
` lastAdmittedPromptAt: ${formatTimestamp(session.entry.lastAdmittedPromptAt)}`
|
|
83661
|
-
].join(`
|
|
84466
|
+
].filter((line) => Boolean(line)).join(`
|
|
83662
84467
|
`);
|
|
83663
84468
|
}
|
|
83664
84469
|
function resolveIndexedSession(sessions, index) {
|
|
@@ -83697,6 +84502,7 @@ async function resolveWatchSelection(options) {
|
|
|
83697
84502
|
const sessionMetadata = buildRunnerSessionMetadata(context.loadedConfig, context.entries);
|
|
83698
84503
|
if (options.sessionName) {
|
|
83699
84504
|
return {
|
|
84505
|
+
loadedConfig: context.loadedConfig,
|
|
83700
84506
|
sessionName: options.sessionName,
|
|
83701
84507
|
metadata: sessionMetadata.find((item) => item.sessionName === options.sessionName) ?? null,
|
|
83702
84508
|
tmux: context.tmux
|
|
@@ -83705,6 +84511,7 @@ async function resolveWatchSelection(options) {
|
|
|
83705
84511
|
if (options.index != null) {
|
|
83706
84512
|
const selected = resolveIndexedSession(await listRunnerSessions(context.loadedConfig), options.index);
|
|
83707
84513
|
return {
|
|
84514
|
+
loadedConfig: context.loadedConfig,
|
|
83708
84515
|
sessionName: selected.sessionName,
|
|
83709
84516
|
metadata: sessionMetadata.find((item) => item.sessionName === selected.sessionName) ?? null,
|
|
83710
84517
|
tmux: context.tmux
|
|
@@ -83716,6 +84523,7 @@ async function resolveWatchSelection(options) {
|
|
|
83716
84523
|
throw new CliCommandError(`No admitted prompt is recorded yet. Use ${renderCliCommand("runner watch --next", { inline: true })} or watch a named session.`, 1);
|
|
83717
84524
|
}
|
|
83718
84525
|
return {
|
|
84526
|
+
loadedConfig: context.loadedConfig,
|
|
83719
84527
|
sessionName: latest.sessionName,
|
|
83720
84528
|
metadata: latest,
|
|
83721
84529
|
tmux: context.tmux
|
|
@@ -83735,6 +84543,7 @@ async function resolveWatchSelection(options) {
|
|
|
83735
84543
|
});
|
|
83736
84544
|
if (admittedAfterBaseline[0]) {
|
|
83737
84545
|
return {
|
|
84546
|
+
loadedConfig: context.loadedConfig,
|
|
83738
84547
|
sessionName: admittedAfterBaseline[0].sessionName,
|
|
83739
84548
|
metadata: admittedAfterBaseline[0],
|
|
83740
84549
|
tmux: context.tmux
|
|
@@ -83765,13 +84574,18 @@ async function runWatchCli(args) {
|
|
|
83765
84574
|
throw error;
|
|
83766
84575
|
}
|
|
83767
84576
|
}
|
|
84577
|
+
const identity = deriveRunnerSessionIdentity({
|
|
84578
|
+
entry: selection.metadata?.entry,
|
|
84579
|
+
liveSessionId: parseRunnerSessionIdFromSnapshot(selection.loadedConfig, selection.metadata?.entry, snapshot)
|
|
84580
|
+
});
|
|
83768
84581
|
const frame = renderWatchFrame({
|
|
83769
84582
|
sessionName: selection.sessionName,
|
|
83770
|
-
|
|
84583
|
+
sessionId: identity.sessionId,
|
|
84584
|
+
sessionIdPersistence: identity.sessionIdPersistence,
|
|
84585
|
+
storedSessionId: identity.storedSessionId,
|
|
83771
84586
|
agentId: selection.metadata?.entry.agentId,
|
|
83772
84587
|
lines: options.lines,
|
|
83773
|
-
|
|
83774
|
-
status,
|
|
84588
|
+
state: status,
|
|
83775
84589
|
snapshot
|
|
83776
84590
|
});
|
|
83777
84591
|
if (process.stdout.isTTY) {
|
|
@@ -83887,6 +84701,7 @@ var GITHUB_RAW_BASE = "https://raw.githubusercontent.com/longbkit/clisbot/main";
|
|
|
83887
84701
|
function renderUpdateHelp() {
|
|
83888
84702
|
return [
|
|
83889
84703
|
`${renderCliCommand("update")} / ${renderCliCommand("update --help")}`,
|
|
84704
|
+
"Start here for any clisbot install or update request.",
|
|
83890
84705
|
"Prints this guide only. Direct update is not supported yet.",
|
|
83891
84706
|
"A bot can use this guide to update itself.",
|
|
83892
84707
|
"",
|
|
@@ -83897,21 +84712,30 @@ function renderUpdateHelp() {
|
|
|
83897
84712
|
"",
|
|
83898
84713
|
"Flow:",
|
|
83899
84714
|
` 1. ${renderCliCommand("status")}`,
|
|
83900
|
-
" 2. Read docs in priority order.",
|
|
84715
|
+
" 2. Read docs in priority order and follow them before installing.",
|
|
83901
84716
|
" 3. npm install -g clisbot@<target> && clisbot restart",
|
|
83902
84717
|
` 4. ${renderCliCommand("status")}`,
|
|
83903
|
-
" 5. Report version, health, manual action, and useful release
|
|
84718
|
+
" 5. Report version, health, manual action, and useful release highlights.",
|
|
83904
84719
|
"",
|
|
83905
84720
|
"Docs, read in order:",
|
|
83906
84721
|
` 1. Migration index: ${GITHUB_RAW_BASE}/docs/migrations/index.md`,
|
|
83907
84722
|
" If Manual action: required, follow its runbook. If none, continue.",
|
|
83908
|
-
` 2.
|
|
83909
|
-
" Use for
|
|
83910
|
-
` 3.
|
|
83911
|
-
" Use for
|
|
83912
|
-
|
|
84723
|
+
` 2. Update guide: ${GITHUB_RAW_BASE}/docs/updates/update-guide.md`,
|
|
84724
|
+
" Use for target choice, install flow, verification, and wrong-publish recovery.",
|
|
84725
|
+
` 3. Release notes: ${GITHUB_RAW_BASE}/docs/releases/README.md`,
|
|
84726
|
+
" Use for the canonical version map and full version notes.",
|
|
84727
|
+
` 4. Release guides: ${GITHUB_RAW_BASE}/docs/updates/README.md`,
|
|
84728
|
+
" Use for shorter catch-up notes: what changed, what to try, and what to watch.",
|
|
84729
|
+
" 5. Full docs: https://github.com/longbkit/clisbot/tree/main/docs",
|
|
83913
84730
|
" Use for deep questions. If needed, fetch or clone docs and inspect relevant files.",
|
|
83914
84731
|
"",
|
|
84732
|
+
"Recovery:",
|
|
84733
|
+
" - If a version was published by mistake, publish the corrected target or tag first.",
|
|
84734
|
+
" - Then deprecate the wrong version.",
|
|
84735
|
+
" - Start with `npm login` in an attached session.",
|
|
84736
|
+
" - If npm returns a browser approval URL, keep that same session open and continue it after approval.",
|
|
84737
|
+
" - If the write command still returns EOTP, ask the operator for a current OTP and rerun the exact command with --otp=<code>.",
|
|
84738
|
+
"",
|
|
83915
84739
|
"Rules:",
|
|
83916
84740
|
" - Use npm dist-tags, not highest semver.",
|
|
83917
84741
|
" - Stable/latest is default; beta only when the user asks.",
|
|
@@ -84220,7 +85044,7 @@ function appendBootstrapGuidance(lines, summary) {
|
|
|
84220
85044
|
lines.push(` Agent ${agent.id} still needs bootstrap completion.`);
|
|
84221
85045
|
lines.push(` workspace: ${agent.workspacePath}`);
|
|
84222
85046
|
lines.push(" next: chat with the bot or open the workspace");
|
|
84223
|
-
lines.push(
|
|
85047
|
+
lines.push(" follow: BOOTSTRAP.md, AGENTS.md, and the rest of the seeded workspace files");
|
|
84224
85048
|
}
|
|
84225
85049
|
lines.push("");
|
|
84226
85050
|
lines.push(" Next steps after bootstrap:");
|
|
@@ -84269,7 +85093,7 @@ function renderStartSummary(summary) {
|
|
|
84269
85093
|
lines.push(` Example: ${renderCliCommand("start --cli codex --bot-type team")}`);
|
|
84270
85094
|
lines.push(` Manual setup is still available with ${renderCliCommand("agents add ...", { inline: true })}.`);
|
|
84271
85095
|
lines.push(...renderOperatorHelpLines(" "));
|
|
84272
|
-
lines.push(" Bootstrap files
|
|
85096
|
+
lines.push(" Bootstrap files are optional. If you use `--bot-type`, clisbot seeds BOOTSTRAP.md, AGENTS.md, SOUL.md, USER.md, IDENTITY.md, and related files into the agent workspace.");
|
|
84273
85097
|
return lines.join(`
|
|
84274
85098
|
`);
|
|
84275
85099
|
}
|
|
@@ -84465,7 +85289,7 @@ async function getRuntimeOperatorSummary(params) {
|
|
|
84465
85289
|
},
|
|
84466
85290
|
agentSummaries,
|
|
84467
85291
|
channelSummaries,
|
|
84468
|
-
activeRuns: await agentService.
|
|
85292
|
+
activeRuns: await agentService.listLiveSessionRuntimes(),
|
|
84469
85293
|
configuredAgents: agentSummaries.length,
|
|
84470
85294
|
bootstrapPendingAgents: agentSummaries.filter((item) => item.bootstrapState === "missing" || item.bootstrapState === "not-bootstrapped").length,
|
|
84471
85295
|
bootstrappedAgents: agentSummaries.filter((item) => item.bootstrapState === "bootstrapped").length,
|
|
@@ -84547,7 +85371,10 @@ function renderBootstrapCommandHelp(commandName) {
|
|
|
84547
85371
|
"Behavior:",
|
|
84548
85372
|
` - ${behavior}`,
|
|
84549
85373
|
" - first-run agent bootstrap needs both `--cli` and `--bot-type`",
|
|
85374
|
+
" - `--bot-type` is a workspace-template choice, not a general runtime requirement for later agents",
|
|
84550
85375
|
" - `--bot-type personal` maps to `personal-assistant`; `--bot-type team` maps to `team-assistant`",
|
|
85376
|
+
" - bootstrap seeds `AGENTS.md`, `BOOTSTRAP.md`, `IDENTITY.md`, and the rest of the workspace guidance files",
|
|
85377
|
+
" - Claude and Gemini bootstraps also create `CLAUDE.md` or `GEMINI.md` as symlinks to `AGENTS.md`",
|
|
84551
85378
|
" - explicit credential flags only enable the channels and bots you named in this command",
|
|
84552
85379
|
" - env-style values such as `SLACK_APP_TOKEN` or `${SLACK_APP_TOKEN}` stay env-backed in config",
|
|
84553
85380
|
commandName === "start" ? " - literal token values without `--persist` stay runtime-only for this start invocation" : " - literal token values on `init` require `--persist` because no runtime exists yet",
|
|
@@ -85491,6 +86318,8 @@ async function withStartupTimeout(name, start2) {
|
|
|
85491
86318
|
}
|
|
85492
86319
|
|
|
85493
86320
|
// src/control/runtime-management-cli.ts
|
|
86321
|
+
var RESTART_STOP_STATUS_RECHECK_TIMEOUT_MS = 2000;
|
|
86322
|
+
var RESTART_STOP_STATUS_RECHECK_INTERVAL_MS = 100;
|
|
85494
86323
|
function getOperatorConfigPath() {
|
|
85495
86324
|
return expandHomePath(process.env.CLISBOT_CONFIG_PATH || DEFAULT_CONFIG_PATH);
|
|
85496
86325
|
}
|
|
@@ -85727,11 +86556,36 @@ async function stop(hard = false) {
|
|
|
85727
86556
|
console.log("clisbot stopped");
|
|
85728
86557
|
printCommandOutcomeFooter("success");
|
|
85729
86558
|
}
|
|
85730
|
-
async function restart(
|
|
85731
|
-
|
|
85732
|
-
|
|
85733
|
-
|
|
85734
|
-
|
|
86559
|
+
async function restart(dependencies = {
|
|
86560
|
+
stopDetachedRuntime,
|
|
86561
|
+
getRuntimeStatus,
|
|
86562
|
+
sleep,
|
|
86563
|
+
warn: (message) => console.error(message)
|
|
86564
|
+
}) {
|
|
86565
|
+
const configPath = getOperatorConfigPath();
|
|
86566
|
+
try {
|
|
86567
|
+
await dependencies.stopDetachedRuntime({
|
|
86568
|
+
configPath,
|
|
86569
|
+
hard: false
|
|
86570
|
+
});
|
|
86571
|
+
} catch (error) {
|
|
86572
|
+
const status = await waitForStoppedRuntimeAfterStopError(configPath, dependencies);
|
|
86573
|
+
if (status.running) {
|
|
86574
|
+
throw error;
|
|
86575
|
+
}
|
|
86576
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
86577
|
+
dependencies.warn(`warning: clisbot stop reported an error, but status now shows the service is stopped; continuing with start. Stop error: ${message}`);
|
|
86578
|
+
}
|
|
86579
|
+
}
|
|
86580
|
+
async function waitForStoppedRuntimeAfterStopError(configPath, dependencies) {
|
|
86581
|
+
const deadline = Date.now() + RESTART_STOP_STATUS_RECHECK_TIMEOUT_MS;
|
|
86582
|
+
while (true) {
|
|
86583
|
+
const status = await dependencies.getRuntimeStatus({ configPath });
|
|
86584
|
+
if (!status.running || Date.now() >= deadline) {
|
|
86585
|
+
return status;
|
|
86586
|
+
}
|
|
86587
|
+
await dependencies.sleep(RESTART_STOP_STATUS_RECHECK_INTERVAL_MS);
|
|
86588
|
+
}
|
|
85735
86589
|
}
|
|
85736
86590
|
async function status() {
|
|
85737
86591
|
await printStatusSummary();
|