@link-assistant/hive-mind 2.10.0 → 2.10.2
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/CHANGELOG.md +12 -0
- package/README.hi.md +2 -0
- package/README.md +2 -0
- package/README.ru.md +2 -0
- package/README.zh.md +2 -0
- package/package.json +1 -1
- package/src/claude.lib.mjs +0 -4
- package/src/cleanup.mjs +18 -6
- package/src/codex.lib.mjs +0 -4
- package/src/configure-claude.mjs +3 -0
- package/src/credential-sanitization-core.lib.mjs +231 -0
- package/src/development-log.lib.mjs +39 -6
- package/src/fix.mjs +3 -0
- package/src/github-error-reporter.lib.mjs +13 -8
- package/src/github-issue-auto-close.lib.mjs +2 -1
- package/src/github-merge-issue-close.lib.mjs +2 -1
- package/src/github.lib.mjs +29 -18
- package/src/hive-screens.mjs +3 -0
- package/src/instrument.mjs +14 -0
- package/src/interactive-mode.lib.mjs +25 -40
- package/src/lib.mjs +89 -50
- package/src/log-upload.lib.mjs +22 -4
- package/src/post-finish-sanitization-sweep.lib.mjs +5 -5
- package/src/review.mjs +3 -1
- package/src/sentry.lib.mjs +27 -8
- package/src/session-monitor.lib.mjs +6 -10
- package/src/session-resume.lib.mjs +43 -23
- package/src/session-store.lib.mjs +3 -1
- package/src/solve.auto-pr.lib.mjs +11 -21
- package/src/solve.error-handlers.lib.mjs +2 -1
- package/src/solve.progress-monitoring.lib.mjs +20 -9
- package/src/solve.results.lib.mjs +9 -15
- package/src/start-screen.mjs +3 -0
- package/src/task.issue-creation.lib.mjs +5 -3
- package/src/task.mjs +21 -8
- package/src/telegram-bot.mjs +6 -3
- package/src/telegram-command-execution.lib.mjs +2 -2
- package/src/telegram-isolation.lib.mjs +2 -0
- package/src/telegram-log-command.lib.mjs +38 -4
- package/src/telegram-safe-reply.lib.mjs +7 -4
- package/src/telegram-solve-queue.lib.mjs +5 -6
- package/src/telegram-tokens-command.lib.mjs +1 -1
- package/src/token-sanitization.lib.mjs +177 -14
- package/src/tool-comments.lib.mjs +5 -5
- package/src/youtrack/youtrack-sync.mjs +4 -3
package/src/sentry.lib.mjs
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
// Sentry integration library for hive-mind
|
|
2
2
|
import { isSentryEnabled, captureException, captureMessage, startTransaction } from './instrument.mjs';
|
|
3
|
+
import { sanitizeCredentialText } from './credential-sanitization-core.lib.mjs';
|
|
4
|
+
|
|
5
|
+
const sanitizeError = error => {
|
|
6
|
+
const source = error instanceof Error ? error : new Error(String(error));
|
|
7
|
+
const sanitized = new Error(sanitizeCredentialText(source.message));
|
|
8
|
+
sanitized.name = source.name;
|
|
9
|
+
if (source.stack) sanitized.stack = sanitizeCredentialText(source.stack);
|
|
10
|
+
return sanitized;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const sanitizeContext = (value, seen = new WeakSet()) => {
|
|
14
|
+
if (typeof value === 'string') return sanitizeCredentialText(value);
|
|
15
|
+
if (value instanceof Error) return sanitizeError(value);
|
|
16
|
+
if (!value || typeof value !== 'object') return value;
|
|
17
|
+
if (seen.has(value)) return '[Circular]';
|
|
18
|
+
seen.add(value);
|
|
19
|
+
if (Array.isArray(value)) return value.map(item => sanitizeContext(item, seen));
|
|
20
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, sanitizeContext(item, seen)]));
|
|
21
|
+
};
|
|
3
22
|
|
|
4
23
|
// Lazy import of Sentry to handle cases where it's not installed
|
|
5
24
|
let Sentry = null;
|
|
@@ -84,7 +103,7 @@ export const withSentry = (fn, name, op = 'task') => {
|
|
|
84
103
|
return result;
|
|
85
104
|
} catch (error) {
|
|
86
105
|
transaction.setStatus('internal_error');
|
|
87
|
-
captureException(error, {
|
|
106
|
+
captureException(sanitizeError(error), {
|
|
88
107
|
operation: name,
|
|
89
108
|
args: args.length > 0 ? `${args.length} arguments` : 'no arguments',
|
|
90
109
|
});
|
|
@@ -139,7 +158,7 @@ export const logToSentry = (message, level = 'info', context = {}) => {
|
|
|
139
158
|
return;
|
|
140
159
|
}
|
|
141
160
|
|
|
142
|
-
captureMessage(message, level, context);
|
|
161
|
+
captureMessage(sanitizeCredentialText(message), level, sanitizeContext(context));
|
|
143
162
|
};
|
|
144
163
|
|
|
145
164
|
/**
|
|
@@ -153,7 +172,7 @@ export const reportError = (error, context = {}) => {
|
|
|
153
172
|
return;
|
|
154
173
|
}
|
|
155
174
|
|
|
156
|
-
captureException(error, { ...context, level: 'error' });
|
|
175
|
+
captureException(sanitizeError(error), { ...sanitizeContext(context), level: 'error' });
|
|
157
176
|
};
|
|
158
177
|
|
|
159
178
|
/**
|
|
@@ -169,7 +188,7 @@ export const reportWarning = (warning, context = {}) => {
|
|
|
169
188
|
|
|
170
189
|
// Convert string warnings to Error objects for better stack traces
|
|
171
190
|
const warningError = typeof warning === 'string' ? new Error(warning) : warning;
|
|
172
|
-
captureException(warningError, { ...context, level: 'warning' });
|
|
191
|
+
captureException(sanitizeError(warningError), { ...sanitizeContext(context), level: 'warning' });
|
|
173
192
|
};
|
|
174
193
|
|
|
175
194
|
/**
|
|
@@ -183,7 +202,7 @@ export const addBreadcrumb = async breadcrumb => {
|
|
|
183
202
|
|
|
184
203
|
const sentry = await getSentry();
|
|
185
204
|
if (sentry) {
|
|
186
|
-
sentry.addBreadcrumb(breadcrumb);
|
|
205
|
+
sentry.addBreadcrumb(sanitizeContext(breadcrumb));
|
|
187
206
|
}
|
|
188
207
|
};
|
|
189
208
|
|
|
@@ -198,7 +217,7 @@ export const setUserContext = async user => {
|
|
|
198
217
|
|
|
199
218
|
const sentry = await getSentry();
|
|
200
219
|
if (sentry) {
|
|
201
|
-
sentry.setUser(user);
|
|
220
|
+
sentry.setUser(sanitizeContext(user));
|
|
202
221
|
}
|
|
203
222
|
};
|
|
204
223
|
|
|
@@ -214,7 +233,7 @@ export const setExtraContext = async (key, value) => {
|
|
|
214
233
|
|
|
215
234
|
const sentry = await getSentry();
|
|
216
235
|
if (sentry) {
|
|
217
|
-
sentry.setExtra(key, value);
|
|
236
|
+
sentry.setExtra(key, sanitizeContext(value));
|
|
218
237
|
}
|
|
219
238
|
};
|
|
220
239
|
|
|
@@ -229,7 +248,7 @@ export const setTags = async tags => {
|
|
|
229
248
|
|
|
230
249
|
const sentry = await getSentry();
|
|
231
250
|
if (sentry) {
|
|
232
|
-
sentry.setTags(tags);
|
|
251
|
+
sentry.setTags(sanitizeContext(tags));
|
|
233
252
|
}
|
|
234
253
|
};
|
|
235
254
|
|
|
@@ -25,8 +25,7 @@ import { promisify } from 'util';
|
|
|
25
25
|
import { formatSessionCompletionMessage, getSessionCompletionExitCode, classifySessionOutcome } from './work-session-formatting.lib.mjs';
|
|
26
26
|
import { notifySubscribers, getSubscriberCount } from './telegram-subscribers.lib.mjs';
|
|
27
27
|
import { classifyExitStatus, normalizeExitCode } from './session-status.lib.mjs';
|
|
28
|
-
import
|
|
29
|
-
import { readLastSessionIdFromLog, findLatestSessionLogId, buildResumeCommand, formatResumeSection } from './session-resume.lib.mjs';
|
|
28
|
+
import { readLastSessionIdFromLog, buildResumeCommand, formatResumeSection } from './session-resume.lib.mjs';
|
|
30
29
|
|
|
31
30
|
export { formatSessionCompletionMessage, getSessionCompletionExitCode } from './work-session-formatting.lib.mjs';
|
|
32
31
|
|
|
@@ -913,14 +912,11 @@ export async function monitorSessions(bot, verbose = false, options = {}) {
|
|
|
913
912
|
const logPath = statusResult?.logPath || sessionInfo?.logPath || null;
|
|
914
913
|
// The id must be the AI TOOL's session id, not the isolation session
|
|
915
914
|
// id (sessionInfo.sessionId — wrong namespace for `solve --resume`).
|
|
916
|
-
//
|
|
917
|
-
//
|
|
918
|
-
//
|
|
919
|
-
//
|
|
920
|
-
|
|
921
|
-
if (!lastSessionId && logPath) {
|
|
922
|
-
lastSessionId = findLatestSessionLogId({ dir: path.dirname(logPath), verbose });
|
|
923
|
-
}
|
|
915
|
+
// Scan backwards through this task's captured log for its last
|
|
916
|
+
// `Session ID:` marker. Do not guess from neighboring UUID-named
|
|
917
|
+
// logs: start-command stores unrelated tasks in the same backend
|
|
918
|
+
// directory, which caused issue #2109's invalid resume id.
|
|
919
|
+
const lastSessionId = readLastSessionIdFromLog(logPath, { verbose });
|
|
924
920
|
const resumeCommand = buildResumeCommand({ sessionInfo, lastSessionId });
|
|
925
921
|
const resumeSection = formatResumeSection({ lastSessionId, command: resumeCommand });
|
|
926
922
|
if (resumeSection) {
|
|
@@ -11,11 +11,10 @@
|
|
|
11
11
|
* 1. **Use the LAST session id.** A single `/solve` run can spin up *many*
|
|
12
12
|
* tool sessions — auto-continue across usage-limit resets, uncommitted-
|
|
13
13
|
* changes restarts (`solve.watch`), and manual `--resume` chains. Every one
|
|
14
|
-
* prints a `Session ID:` marker to the captured log in chronological order
|
|
15
|
-
* and start-command also renames the per-session log to `<sessionId>.log`.
|
|
14
|
+
* prints a `Session ID:` marker to the captured log in chronological order.
|
|
16
15
|
* The most advanced context lives in the *last* of these, so resuming must
|
|
17
|
-
* pick the last id — never the first. {@link selectLastSessionId}
|
|
18
|
-
* {@link
|
|
16
|
+
* pick the last id — never the first. {@link selectLastSessionId} and
|
|
17
|
+
* {@link readLastSessionIdFromLog} enforce that rule.
|
|
19
18
|
*
|
|
20
19
|
* 2. **Never storm.** Auto-resuming a killed session must be bounded so a job
|
|
21
20
|
* that reliably OOMs cannot spawn an infinite relaunch loop (which would be
|
|
@@ -41,6 +40,8 @@ const SESSION_ID_MARKER_RE = /Session ID:\s*`?([^\s`]+)`?/gi;
|
|
|
41
40
|
// `<sessionId>.log` files start-command writes. Used to validate directory
|
|
42
41
|
// scans so unrelated `*.log` files are never mistaken for a session.
|
|
43
42
|
const SESSION_LOG_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
43
|
+
const DEFAULT_LOG_SCAN_CHUNK_BYTES = 262144;
|
|
44
|
+
const LOG_SCAN_OVERLAP_BYTES = 1024;
|
|
44
45
|
|
|
45
46
|
/**
|
|
46
47
|
* Extract every tool session id printed to a log, in the order they appear.
|
|
@@ -79,34 +80,52 @@ export function selectLastSessionId(text) {
|
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
/**
|
|
82
|
-
* Read the LAST tool session id from a `/solve` execution log.
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
83
|
+
* Read the LAST tool session id from a `/solve` execution log. The file is
|
|
84
|
+
* scanned backwards in bounded chunks: this normally stops after the tail
|
|
85
|
+
* chunk, while still finding a valid marker that precedes a long tool trace.
|
|
86
|
+
* Adjacent chunks overlap so a marker split at a chunk boundary is not lost.
|
|
87
|
+
* Never throws — a missing/unreadable log yields `null`.
|
|
86
88
|
*
|
|
87
89
|
* @param {string} logPath
|
|
88
90
|
* @param {Object} [options]
|
|
89
91
|
* @param {Object} [options.fsImpl=fs] - Injectable fs (for tests)
|
|
90
|
-
* @param {number} [options.tailBytes=262144] -
|
|
92
|
+
* @param {number} [options.tailBytes=262144] - Bytes per backwards scan chunk
|
|
91
93
|
* @param {boolean} [options.verbose]
|
|
92
94
|
* @returns {string|null}
|
|
93
95
|
*/
|
|
94
96
|
export function readLastSessionIdFromLog(logPath, options = {}) {
|
|
95
|
-
const { fsImpl = fs, tailBytes =
|
|
97
|
+
const { fsImpl = fs, tailBytes = DEFAULT_LOG_SCAN_CHUNK_BYTES, verbose = false } = options;
|
|
96
98
|
if (!logPath) return null;
|
|
97
99
|
try {
|
|
98
100
|
const stat = fsImpl.statSync(logPath);
|
|
99
|
-
const
|
|
101
|
+
const chunkBytes = Number.isFinite(tailBytes) && tailBytes > 0 ? Math.floor(tailBytes) : DEFAULT_LOG_SCAN_CHUNK_BYTES;
|
|
100
102
|
const fd = fsImpl.openSync(logPath, 'r');
|
|
101
103
|
try {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
104
|
+
let end = stat.size;
|
|
105
|
+
let laterPrefix = Buffer.alloc(0);
|
|
106
|
+
let chunksScanned = 0;
|
|
107
|
+
while (end > 0) {
|
|
108
|
+
const start = Math.max(0, end - chunkBytes);
|
|
109
|
+
const length = end - start;
|
|
110
|
+
const buffer = Buffer.alloc(length);
|
|
111
|
+
const bytesRead = fsImpl.readSync(fd, buffer, 0, length, start);
|
|
112
|
+
const current = bytesRead === length ? buffer : buffer.subarray(0, bytesRead);
|
|
113
|
+
const scanBuffer = laterPrefix.length > 0 ? Buffer.concat([current, laterPrefix]) : current;
|
|
114
|
+
const id = selectLastSessionId(scanBuffer.toString('utf8'));
|
|
115
|
+
chunksScanned += 1;
|
|
116
|
+
if (id) {
|
|
117
|
+
if (verbose) {
|
|
118
|
+
console.log(`[VERBOSE] session-resume: last tool session id in ${logPath} is ${id} (scanned ${chunksScanned} chunk${chunksScanned === 1 ? '' : 's'})`);
|
|
119
|
+
}
|
|
120
|
+
return id;
|
|
121
|
+
}
|
|
122
|
+
laterPrefix = current.subarray(0, Math.min(current.length, LOG_SCAN_OVERLAP_BYTES));
|
|
123
|
+
end = start;
|
|
108
124
|
}
|
|
109
|
-
|
|
125
|
+
if (verbose) {
|
|
126
|
+
console.log(`[VERBOSE] session-resume: no tool session id found in ${logPath} after scanning ${chunksScanned} chunk${chunksScanned === 1 ? '' : 's'}`);
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
110
129
|
} finally {
|
|
111
130
|
fsImpl.closeSync(fd);
|
|
112
131
|
}
|
|
@@ -121,10 +140,10 @@ export function readLastSessionIdFromLog(logPath, options = {}) {
|
|
|
121
140
|
/**
|
|
122
141
|
* Find the id of the most-recently-modified `<sessionId>.log` in a directory.
|
|
123
142
|
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
143
|
+
* This helper is safe only when `dir` is already known to contain logs for one
|
|
144
|
+
* task. A shared start-command directory cannot attribute its newest UUID log
|
|
145
|
+
* to a particular `/solve` run, so completion notifications must not use this
|
|
146
|
+
* as a fallback. Never throws.
|
|
128
147
|
*
|
|
129
148
|
* @param {Object} options
|
|
130
149
|
* @param {string} options.dir - Directory holding `<sessionId>.log` files
|
|
@@ -215,7 +234,8 @@ export function buildResumeCommand({ sessionInfo = {}, lastSessionId = null, bin
|
|
|
215
234
|
const url = sessionInfo.url || (Array.isArray(sessionInfo.args) ? sessionInfo.args[0] : null);
|
|
216
235
|
if (!url) return null;
|
|
217
236
|
|
|
218
|
-
const
|
|
237
|
+
const commandAlias = typeof sessionInfo.commandAlias === 'string' && /^[a-z0-9_-]+$/i.test(sessionInfo.commandAlias) ? sessionInfo.commandAlias : null;
|
|
238
|
+
const bin = binary || (commandAlias ? `/${commandAlias}` : command);
|
|
219
239
|
let args;
|
|
220
240
|
if (Array.isArray(sessionInfo.args) && sessionInfo.args.length > 0) {
|
|
221
241
|
args = stripResumeFlag(sessionInfo.args);
|
|
@@ -33,7 +33,9 @@ import path from 'node:path';
|
|
|
33
33
|
// excluded so the snapshot stays small and safe to reload.
|
|
34
34
|
// `args` (#1927 review follow-up) is persisted so a killed /solve can be resumed
|
|
35
35
|
// with its exact original invocation plus `--resume <lastSessionId>`.
|
|
36
|
-
|
|
36
|
+
// `commandAlias` (#2109) preserves the Telegram spelling (`solve`, `codex`,
|
|
37
|
+
// `claude`, etc.) so a bot notification never suggests a terminal-only command.
|
|
38
|
+
const PERSISTABLE_FIELDS = ['chatId', 'messageId', 'startTime', 'url', 'command', 'commandAlias', 'isolationBackend', 'sessionId', 'containerFilesystemStartBytes', 'containerFilesystemLastBytes', 'containerFilesystemLastObservedAt', 'tool', 'infoBlock', 'urlContext', 'requesterUserId', 'showLimits', 'locale', 'logPath', 'args'];
|
|
37
39
|
|
|
38
40
|
/**
|
|
39
41
|
* Resolve the directory durable bot state is written to. Honors
|
|
@@ -11,6 +11,7 @@ import { handleCompareApiNotReady } from './solve.auto-pr-compare-readiness.lib.
|
|
|
11
11
|
|
|
12
12
|
import { wrapDollarWithGhRetry as _wrapDollarWithGhRetry, execGhWithRetry, isTransientCompareApiError } from './github-rate-limit.lib.mjs'; // rate-limit marker (#1726): gh API calls flow through $ wrapped by caller. Issue #1756: execGhWithRetry retries on transient 5xx (504) too. Issue #1829: isTransientCompareApiError lets the compare-API readiness gate degrade gracefully on transient diff-render failures.
|
|
13
13
|
import { stagePlaceholderFileOrExplain, explainNothingStagedAndThrow } from './solve.auto-pr-placeholder.lib.mjs'; // Issue #1825: handles the seed placeholder when the target repo gitignores it.
|
|
14
|
+
import { sanitizeForPublication, writeSanitizedPublicationFile } from './token-sanitization.lib.mjs';
|
|
14
15
|
|
|
15
16
|
export async function handleAutoPrCreation({ argv, tempDir, branchName, issueNumber, owner, repo, defaultBranch, forkedRepo, isContinueMode, prNumber, log, formatAligned, $, reportError, path, fs }) {
|
|
16
17
|
// Skip auto-PR creation if:
|
|
@@ -906,17 +907,19 @@ ${prBody}`,
|
|
|
906
907
|
// single transient 5xx (e.g. `HTTP 504: 504 Gateway Timeout
|
|
907
908
|
// (https://api.github.com/graphql)`) or rate-limit response retries
|
|
908
909
|
// instead of aborting the whole solve session.
|
|
910
|
+
let prBodyFile = null;
|
|
911
|
+
let prTitleFile = null;
|
|
909
912
|
try {
|
|
910
913
|
// Write PR body to temp file to avoid shell escaping issues
|
|
911
|
-
|
|
912
|
-
await
|
|
914
|
+
prBodyFile = `/tmp/pr-body-${Date.now()}.md`;
|
|
915
|
+
await writeSanitizedPublicationFile(prBodyFile, prBody);
|
|
913
916
|
|
|
914
917
|
// Write PR title to temp file to avoid shell escaping issues with quotes/apostrophes
|
|
915
918
|
// This solves the issue where titles containing apostrophes (e.g., "don't") would cause
|
|
916
919
|
// "Unterminated quoted string" errors
|
|
917
|
-
const prTitle = `[WIP] ${issueTitle}
|
|
918
|
-
|
|
919
|
-
await
|
|
920
|
+
const prTitle = await sanitizeForPublication(`[WIP] ${issueTitle}`);
|
|
921
|
+
prTitleFile = `/tmp/pr-title-${Date.now()}.txt`;
|
|
922
|
+
await writeSanitizedPublicationFile(prTitleFile, prTitle);
|
|
920
923
|
|
|
921
924
|
// Build command with optional assignee and handle forks
|
|
922
925
|
// Note: targetBranch is already defined above
|
|
@@ -994,22 +997,6 @@ ${prBody}`,
|
|
|
994
997
|
}
|
|
995
998
|
}
|
|
996
999
|
|
|
997
|
-
// Clean up temp files
|
|
998
|
-
await fs.unlink(prBodyFile).catch(unlinkError => {
|
|
999
|
-
reportError(unlinkError, {
|
|
1000
|
-
context: 'pr_body_file_cleanup',
|
|
1001
|
-
prBodyFile,
|
|
1002
|
-
operation: 'delete_temp_file',
|
|
1003
|
-
});
|
|
1004
|
-
});
|
|
1005
|
-
await fs.unlink(prTitleFile).catch(unlinkError => {
|
|
1006
|
-
reportError(unlinkError, {
|
|
1007
|
-
context: 'pr_title_file_cleanup',
|
|
1008
|
-
prTitleFile,
|
|
1009
|
-
operation: 'delete_temp_file',
|
|
1010
|
-
});
|
|
1011
|
-
});
|
|
1012
|
-
|
|
1013
1000
|
// Log gh pr create output for debugging (Issue #1462)
|
|
1014
1001
|
if (argv.verbose) {
|
|
1015
1002
|
await log(` gh pr create stdout: ${(output || '').trim() || '(empty)'}`, { verbose: true });
|
|
@@ -1279,6 +1266,9 @@ ${prBody}`,
|
|
|
1279
1266
|
} else {
|
|
1280
1267
|
throw new Error(`PR creation failed: ${cleanError}`, { cause: prCreateError });
|
|
1281
1268
|
}
|
|
1269
|
+
} finally {
|
|
1270
|
+
if (prBodyFile) await fs.unlink(prBodyFile).catch(() => {});
|
|
1271
|
+
if (prTitleFile) await fs.unlink(prTitleFile).catch(() => {});
|
|
1282
1272
|
}
|
|
1283
1273
|
}
|
|
1284
1274
|
}
|
|
@@ -11,6 +11,7 @@ import { reportError } from './sentry.lib.mjs';
|
|
|
11
11
|
|
|
12
12
|
// Import GitHub error reporter
|
|
13
13
|
import { handleErrorWithIssueCreation } from './github-error-reporter.lib.mjs';
|
|
14
|
+
import { sanitizeForPublication } from './token-sanitization.lib.mjs';
|
|
14
15
|
|
|
15
16
|
export const isErrorIssueAutoCreationDisabled = argv => !!(argv?.disableReportIssue || argv?.disableIssueAutoCreationOnError);
|
|
16
17
|
|
|
@@ -115,7 +116,7 @@ export const handleFailure = async options => {
|
|
|
115
116
|
if (argv.autoClosePullRequestOnFail && global.createdPR && global.createdPR.number) {
|
|
116
117
|
await log('\n🔒 Auto-closing pull request due to failure...');
|
|
117
118
|
try {
|
|
118
|
-
const closeMessage = errorType === 'uncaughtException' ? 'Auto-closed due to uncaught exception. Logs have been attached for debugging.' : errorType === 'unhandledRejection' ? 'Auto-closed due to unhandled rejection. Logs have been attached for debugging.' : 'Auto-closed due to execution failure. Logs have been attached for debugging.';
|
|
119
|
+
const closeMessage = await sanitizeForPublication(errorType === 'uncaughtException' ? 'Auto-closed due to uncaught exception. Logs have been attached for debugging.' : errorType === 'unhandledRejection' ? 'Auto-closed due to unhandled rejection. Logs have been attached for debugging.' : 'Auto-closed due to execution failure. Logs have been attached for debugging.');
|
|
119
120
|
|
|
120
121
|
const result = await $`gh pr close ${global.createdPR.number} --repo ${global.owner || owner}/${global.repo || repo} --comment ${closeMessage}`;
|
|
121
122
|
if (result.exitCode === 0) {
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
// Issue #1625: centralized markers + tracking helpers so the live-progress
|
|
28
28
|
// comment is excluded from --auto-attach-solution-summary's AI-comment check.
|
|
29
29
|
import { LIVE_PROGRESS_SECTION_START_MARKER, LIVE_PROGRESS_SECTION_END_MARKER, postTrackedCommentFromFile, trackToolCommentId } from './tool-comments.lib.mjs';
|
|
30
|
+
import { writeSanitizedPublicationFile } from './token-sanitization.lib.mjs';
|
|
30
31
|
|
|
31
32
|
import { wrapDollarWithGhRetry as _wrapDollarWithGhRetry } from './github-rate-limit.lib.mjs'; // rate-limit marker (#1726): gh API calls flow through $ wrapped by caller
|
|
32
33
|
/**
|
|
@@ -225,9 +226,12 @@ export const createProgressMonitor = ({ owner, repo, prNumber, $, log, verbose =
|
|
|
225
226
|
// Edit existing comment
|
|
226
227
|
const fs = (await import('fs')).promises;
|
|
227
228
|
const tempFile = `/tmp/pr-progress-comment-${prNumber}-${Date.now()}.md`;
|
|
228
|
-
await
|
|
229
|
-
|
|
230
|
-
|
|
229
|
+
await writeSanitizedPublicationFile(tempFile, progressSection);
|
|
230
|
+
try {
|
|
231
|
+
await $`gh api repos/${owner}/${repo}/issues/comments/${state.commentId} --method PATCH --field body=@${tempFile}`;
|
|
232
|
+
} finally {
|
|
233
|
+
await fs.unlink(tempFile).catch(() => {});
|
|
234
|
+
}
|
|
231
235
|
} else {
|
|
232
236
|
// Create new comment. Issue #1625: post via postTrackedCommentFromFile
|
|
233
237
|
// so the comment ID is captured directly from the GitHub API response
|
|
@@ -235,9 +239,13 @@ export const createProgressMonitor = ({ owner, repo, prNumber, $, log, verbose =
|
|
|
235
239
|
// posted comments from the "did the AI post anything?" check).
|
|
236
240
|
const fs = (await import('fs')).promises;
|
|
237
241
|
const tempFile = `/tmp/pr-progress-comment-${prNumber}-${Date.now()}.md`;
|
|
238
|
-
await
|
|
239
|
-
|
|
240
|
-
|
|
242
|
+
await writeSanitizedPublicationFile(tempFile, progressSection);
|
|
243
|
+
let posted;
|
|
244
|
+
try {
|
|
245
|
+
posted = await postTrackedCommentFromFile({ $, owner, repo, targetNumber: prNumber, bodyFile: tempFile });
|
|
246
|
+
} finally {
|
|
247
|
+
await fs.unlink(tempFile).catch(() => {});
|
|
248
|
+
}
|
|
241
249
|
|
|
242
250
|
if (posted.ok && posted.commentId) {
|
|
243
251
|
state.commentId = posted.commentId;
|
|
@@ -300,9 +308,12 @@ export const createProgressMonitor = ({ owner, repo, prNumber, $, log, verbose =
|
|
|
300
308
|
// Write to temp file and update PR
|
|
301
309
|
const fs = (await import('fs')).promises;
|
|
302
310
|
const tempBodyFile = `/tmp/pr-progress-${prNumber}-${Date.now()}.md`;
|
|
303
|
-
await
|
|
304
|
-
|
|
305
|
-
|
|
311
|
+
await writeSanitizedPublicationFile(tempBodyFile, updatedBody);
|
|
312
|
+
try {
|
|
313
|
+
await $`gh pr edit ${prNumber} --repo ${owner}/${repo} --body-file ${tempBodyFile}`;
|
|
314
|
+
} finally {
|
|
315
|
+
await fs.unlink(tempBodyFile).catch(() => {});
|
|
316
|
+
}
|
|
306
317
|
|
|
307
318
|
const stats = calculateProgress(todos);
|
|
308
319
|
await log(`📊 Updated PR progress: ${stats.percentage}% (${stats.completed}/${stats.total} tasks completed)`);
|
|
@@ -31,10 +31,10 @@ const { sanitizeLogContent, attachLogToGitHub } = githubLib;
|
|
|
31
31
|
|
|
32
32
|
// Issue #1745: process-wide sanitization counters used to print a one-line
|
|
33
33
|
// "we masked N secrets" summary at the end of each run.
|
|
34
|
-
const { formatSanitizationSummary } = await import('./token-sanitization.lib.mjs');
|
|
34
|
+
const { formatSanitizationSummary, sanitizeForPublication, writeSanitizedPublicationFile } = await import('./token-sanitization.lib.mjs');
|
|
35
35
|
// Issue #1745: post-finish retroactive sanitization of bot-authored PR
|
|
36
|
-
// comments and the PR description.
|
|
37
|
-
//
|
|
36
|
+
// comments and the PR description. This external repair boundary always runs
|
|
37
|
+
// when PR coordinates are available.
|
|
38
38
|
const { runPostFinishSweep } = await import('./post-finish-sanitization-sweep.lib.mjs');
|
|
39
39
|
|
|
40
40
|
// Import continuation functions (session resumption, PR detection)
|
|
@@ -153,7 +153,7 @@ export const ensurePullRequestIssueLink = async ({ prNumber, issueNumber, owner,
|
|
|
153
153
|
|
|
154
154
|
const fs = (await use('fs')).promises;
|
|
155
155
|
const tempBodyFile = `/tmp/pr-body-update-${prNumber}-${Date.now()}.md`;
|
|
156
|
-
await
|
|
156
|
+
await writeSanitizedPublicationFile(tempBodyFile, linkResult.body);
|
|
157
157
|
|
|
158
158
|
try {
|
|
159
159
|
const updateResult = await command`gh pr edit ${prNumber} --repo ${owner}/${repo} --body-file "${tempBodyFile}"`;
|
|
@@ -638,25 +638,19 @@ export const showSessionSummary = async (sessionId, limitReached, argv, issueUrl
|
|
|
638
638
|
// Issue #1745: post-finish retroactive sanitization sweep. Re-reads
|
|
639
639
|
// bot-authored PR comments and the PR description, runs them through
|
|
640
640
|
// sanitizeOutput, and edits in place if a leak slipped past the live
|
|
641
|
-
// sanitizer.
|
|
642
|
-
//
|
|
641
|
+
// sanitizer. Publication repair is a strict external boundary, so local
|
|
642
|
+
// diagnostic bypass flags never disable it.
|
|
643
643
|
try {
|
|
644
644
|
const owner = argv.owner;
|
|
645
645
|
const repo = argv.repo;
|
|
646
646
|
const prNumber = argv.prNumber;
|
|
647
|
-
|
|
648
|
-
const skipActiveTokensOutputSanitization = argv['dangerously-skip-active-tokens-output-sanitization'] === true;
|
|
649
|
-
if (owner && repo && prNumber && !skipOutputSanitization) {
|
|
647
|
+
if (owner && repo && prNumber) {
|
|
650
648
|
const sweepResult = await runPostFinishSweep({
|
|
651
649
|
$,
|
|
652
650
|
owner,
|
|
653
651
|
repo,
|
|
654
652
|
prNumber,
|
|
655
653
|
log,
|
|
656
|
-
sanitizationOptions: {
|
|
657
|
-
warnOnMismatch: false,
|
|
658
|
-
skipActiveTokensOutputSanitization,
|
|
659
|
-
},
|
|
660
654
|
});
|
|
661
655
|
if (sweepResult.totalEdited > 0) {
|
|
662
656
|
await log(`🔒 Post-finish sweep: edited ${sweepResult.totalEdited} bot-authored item(s) to mask leaked tokens.`);
|
|
@@ -771,7 +765,7 @@ export const verifyResults = async (owner, repo, branchName, issueNumber, prNumb
|
|
|
771
765
|
// Skip cleanup if auto-restart-on-non-updated-pull-request-description is enabled
|
|
772
766
|
// (let the agent handle it on restart instead)
|
|
773
767
|
if (prTitleHasPlaceholder && !argv.autoRestartOnNonUpdatedPullRequestDescription) {
|
|
774
|
-
const updatedTitle = pr.title.replace(/^\[WIP\]\s*/, '');
|
|
768
|
+
const updatedTitle = await sanitizeForPublication(pr.title.replace(/^\[WIP\]\s*/, ''));
|
|
775
769
|
await log(` 📝 Removing [WIP] prefix from PR title...`);
|
|
776
770
|
const titleResult = await $`gh pr edit ${pr.number} --repo ${owner}/${repo} --title "${updatedTitle}"`;
|
|
777
771
|
if (titleResult.code === 0) {
|
|
@@ -819,7 +813,7 @@ Fixes ${issueRef}
|
|
|
819
813
|
*This PR was created automatically by the AI issue solver*`;
|
|
820
814
|
|
|
821
815
|
const tempBodyFile = `/tmp/pr-body-finalize-${pr.number}-${Date.now()}.md`;
|
|
822
|
-
await
|
|
816
|
+
await writeSanitizedPublicationFile(tempBodyFile, newDescription);
|
|
823
817
|
|
|
824
818
|
try {
|
|
825
819
|
const descResult = await $`gh pr edit ${pr.number} --repo ${owner}/${repo} --body-file "${tempBodyFile}"`;
|
package/src/start-screen.mjs
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
import { exec } from 'child_process';
|
|
5
5
|
import { promisify } from 'util';
|
|
6
6
|
import { parseCliArgumentsWithLino } from './cli-arguments.lib.mjs';
|
|
7
|
+
import { setupStdioLogInterceptor } from './lib.mjs';
|
|
8
|
+
|
|
9
|
+
setupStdioLogInterceptor();
|
|
7
10
|
|
|
8
11
|
const execAsync = promisify(exec);
|
|
9
12
|
|
|
@@ -3,6 +3,7 @@ import path from 'path';
|
|
|
3
3
|
import { spawn } from 'child_process';
|
|
4
4
|
import { promises as fs } from 'fs';
|
|
5
5
|
import { parseGitHubUrl } from './github.lib.mjs';
|
|
6
|
+
import { sanitizeForPublication, writeSanitizedPublicationFile } from './token-sanitization.lib.mjs';
|
|
6
7
|
|
|
7
8
|
export const TASK_ISSUE_TITLE_MAX_LENGTH = 256;
|
|
8
9
|
|
|
@@ -216,9 +217,10 @@ export async function createTaskIssue({ repository, title, body, issueType = nul
|
|
|
216
217
|
const bodyFile = path.join(tempDir, 'body.md');
|
|
217
218
|
|
|
218
219
|
try {
|
|
219
|
-
await
|
|
220
|
+
await writeSanitizedPublicationFile(bodyFile, body);
|
|
221
|
+
const sanitizedTitle = await sanitizeForPublication(title);
|
|
220
222
|
|
|
221
|
-
const result = await run('gh', buildCreateIssueArgs({ repository, title, bodyFile, issueType, labels }));
|
|
223
|
+
const result = await run('gh', buildCreateIssueArgs({ repository, title: sanitizedTitle, bodyFile, issueType, labels }));
|
|
222
224
|
if (result.code === 0) return parseCreatedTaskIssueOutput(result.stdout);
|
|
223
225
|
|
|
224
226
|
const output = `${result.stderr || ''}${result.stdout || ''}`.trim();
|
|
@@ -228,7 +230,7 @@ export async function createTaskIssue({ repository, title, body, issueType = nul
|
|
|
228
230
|
}
|
|
229
231
|
|
|
230
232
|
await log?.(`⚠️ Could not create issue with type/labels (${output || `exit code ${result.code}`}); retrying without them`);
|
|
231
|
-
const retry = await run('gh', buildCreateIssueArgs({ repository, title, bodyFile }));
|
|
233
|
+
const retry = await run('gh', buildCreateIssueArgs({ repository, title: sanitizedTitle, bodyFile }));
|
|
232
234
|
if (retry.code !== 0) {
|
|
233
235
|
const retryOutput = `${retry.stderr || ''}${retry.stdout || ''}`.trim();
|
|
234
236
|
throw new Error(retryOutput || `gh issue create exited with code ${retry.code}`);
|
package/src/task.mjs
CHANGED
|
@@ -8,6 +8,11 @@ import { buildStartAgentArgs, resolveStartAgentCommand } from './task.agent-comm
|
|
|
8
8
|
import { getDefaultTaskModel, parseTaskArguments } from './task.config.lib.mjs';
|
|
9
9
|
import { validateModelName } from './models/index.mjs';
|
|
10
10
|
import { appendOrReplaceParentSplitSection, buildAddSubIssueApiArgs, buildIssueRestIdApiArgs, buildTaskSplitPrompt, buildTaskSplitSystemPrompt, extractTaskSplitJson, formatChildIssueBody, normalizeSplitTasks, parseCreatedIssueUrl, parseTaskIssueUrl } from './task.split.lib.mjs';
|
|
11
|
+
import { setupStdioLogInterceptor } from './lib.mjs';
|
|
12
|
+
import { sanitizeCredentialText } from './credential-sanitization-core.lib.mjs';
|
|
13
|
+
import { sanitizeForPublication } from './token-sanitization.lib.mjs';
|
|
14
|
+
|
|
15
|
+
setupStdioLogInterceptor();
|
|
11
16
|
|
|
12
17
|
const earlyArgs = process.argv.slice(2);
|
|
13
18
|
|
|
@@ -73,10 +78,14 @@ const logFile = path.join(scriptDir, `task-${timestamp}.log`);
|
|
|
73
78
|
async function log(message, options = {}) {
|
|
74
79
|
const { level = 'info', verbose = false } = options;
|
|
75
80
|
if (verbose && !argv.verbose) return;
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
81
|
+
const sanitizedMessage = sanitizeCredentialText(message);
|
|
82
|
+
await fs
|
|
83
|
+
.appendFile(logFile, `[${new Date().toISOString()}] [${level.toUpperCase()}] ${sanitizedMessage}\n`, { mode: 0o600 })
|
|
84
|
+
.then(() => fs.chmod(logFile, 0o600))
|
|
85
|
+
.catch(() => {});
|
|
86
|
+
if (level === 'error') console.error(sanitizedMessage);
|
|
87
|
+
else if (level === 'warning' || level === 'warn') console.warn(sanitizedMessage);
|
|
88
|
+
else console.log(sanitizedMessage);
|
|
80
89
|
}
|
|
81
90
|
|
|
82
91
|
function formatAligned(icon, label, value, indent = 0) {
|
|
@@ -172,7 +181,8 @@ async function fetchIssueRestId(issue) {
|
|
|
172
181
|
}
|
|
173
182
|
|
|
174
183
|
async function createChildIssue(parentIssue, task, index, splitCount) {
|
|
175
|
-
const
|
|
184
|
+
const [safeTitle, safeBody] = await Promise.all([sanitizeForPublication(task.title), sanitizeForPublication(formatChildIssueBody({ parentIssue, task, index, splitCount }))]);
|
|
185
|
+
const args = ['issue', 'create', '--repo', `${parentIssue.owner}/${parentIssue.repo}`, '--title', safeTitle, '--body', safeBody];
|
|
176
186
|
if (parentIssue.labels.length > 0) {
|
|
177
187
|
args.push('--label', parentIssue.labels.join(','));
|
|
178
188
|
}
|
|
@@ -196,9 +206,11 @@ async function linkChildIssue(parentIssue, childIssue) {
|
|
|
196
206
|
|
|
197
207
|
async function updateParentIssue(parentIssue, childIssues) {
|
|
198
208
|
const body = appendOrReplaceParentSplitSection(parentIssue.body, childIssues);
|
|
199
|
-
|
|
209
|
+
const safeBody = await sanitizeForPublication(body);
|
|
210
|
+
await commandOutput('gh', ['issue', 'edit', String(parentIssue.number), '--repo', `${parentIssue.owner}/${parentIssue.repo}`, '--body', safeBody]);
|
|
200
211
|
const childList = childIssues.map(issue => `- #${issue.number} ${issue.title}`).join('\n');
|
|
201
|
-
|
|
212
|
+
const safeComment = await sanitizeForPublication(`Split into ${childIssues.length} tasks:\n\n${childList}`);
|
|
213
|
+
await commandOutput('gh', ['issue', 'comment', String(parentIssue.number), '--repo', `${parentIssue.owner}/${parentIssue.repo}`, '--body', safeComment]);
|
|
202
214
|
}
|
|
203
215
|
|
|
204
216
|
async function runSplitMode() {
|
|
@@ -268,7 +280,8 @@ ${results.clarification ? `Clarification analysis:\n${results.clarification}\n\n
|
|
|
268
280
|
}
|
|
269
281
|
|
|
270
282
|
try {
|
|
271
|
-
await fs.writeFile(logFile, `# Task Log - ${new Date().toISOString()}\n\n
|
|
283
|
+
await fs.writeFile(logFile, `# Task Log - ${new Date().toISOString()}\n\n`, { mode: 0o600 });
|
|
284
|
+
await fs.chmod(logFile, 0o600);
|
|
272
285
|
await log(`📁 Log file: ${logFile}`);
|
|
273
286
|
await log('\n🎯 Task Processing Started');
|
|
274
287
|
await log(formatAligned('📝', 'Task input:', taskInput));
|
package/src/telegram-bot.mjs
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { ensureUseM } from './use-m-bootstrap.lib.mjs';
|
|
3
|
+
import { maskToken, setupStdioLogInterceptor } from './lib.mjs';
|
|
4
|
+
|
|
5
|
+
setupStdioLogInterceptor();
|
|
3
6
|
// Early exit for --version (issue #1318: avoid dotenvx MISSING_ENV_FILE warnings)
|
|
4
7
|
if (process.argv.includes('--version')) {
|
|
5
8
|
const v = await import('./version.lib.mjs').then(m => m.getVersion()).catch(() => 'unknown');
|
|
@@ -212,7 +215,7 @@ if (hiveEnabled && hiveOverrides.length > 0) {
|
|
|
212
215
|
if (config.dryRun) {
|
|
213
216
|
console.log('\n✅ Dry-run mode: All validations passed successfully!');
|
|
214
217
|
console.log('\nConfiguration summary:');
|
|
215
|
-
console.log(' Token:', BOT_TOKEN ?
|
|
218
|
+
console.log(' Token:', BOT_TOKEN ? maskToken(BOT_TOKEN) : 'not set');
|
|
216
219
|
if (allowedChats && allowedChats.length > 0) {
|
|
217
220
|
console.log(' Allowed chats:', lino.format(allowedChats));
|
|
218
221
|
} else {
|
|
@@ -820,13 +823,13 @@ async function handleSolveCommand(ctx) {
|
|
|
820
823
|
|
|
821
824
|
if (check.canStart && check.startReserved) {
|
|
822
825
|
const startingMessage = await safeReply(ctx, formatStartingWorkSessionMessage({ infoBlock, locale: solveLocale }), { reply_to_message_id: ctx.message.message_id });
|
|
823
|
-
await executeAndUpdateMessage(ctx, startingMessage, 'solve', argsWithLocale, infoBlock, effectiveSolveIsolation, solveTool, solveUrlContext, { showLimits: solveShowLimits, limitsAtStart: solveLimitsAtStart, locale: solveLocale });
|
|
826
|
+
await executeAndUpdateMessage(ctx, startingMessage, 'solve', argsWithLocale, infoBlock, effectiveSolveIsolation, solveTool, solveUrlContext, { showLimits: solveShowLimits, limitsAtStart: solveLimitsAtStart, locale: solveLocale, commandAlias: solveCommandName });
|
|
824
827
|
} else {
|
|
825
828
|
if (!solveQueue.executeCallback) {
|
|
826
829
|
const _t = (s, i) => trackSession(s, i, VERBOSE);
|
|
827
830
|
solveQueue.executeCallback = createIsolationAwareQueueCallback(ISOLATION_BACKEND, isolationRunner, _t, createQueueExecuteCallback(executeStartScreen, _t), VERBOSE);
|
|
828
831
|
}
|
|
829
|
-
const queueItem = solveQueue.enqueue({ url: normalizedUrl, args: argsWithLocale, ctx, requester, infoBlock, tool: solveTool, perCommandIsolation: effectiveSolveIsolation, urlContext: solveUrlContext, showLimits: solveShowLimits, limitsAtStart: solveLimitsAtStart, locale: solveLocale });
|
|
832
|
+
const queueItem = solveQueue.enqueue({ url: normalizedUrl, args: argsWithLocale, ctx, requester, infoBlock, commandAlias: solveCommandName, tool: solveTool, perCommandIsolation: effectiveSolveIsolation, urlContext: solveUrlContext, showLimits: solveShowLimits, limitsAtStart: solveLimitsAtStart, locale: solveLocale });
|
|
830
833
|
const queueMessage = buildSolveQueuedMessage({ locale: solveLocale, tool: solveTool, position: toolQueuedCount + 1, infoBlock, reason: check.reason ? escapeMarkdown(check.reason) : '' }); // tool-specific position (#1551)
|
|
831
834
|
const queuedMessage = await safeReply(ctx, queueMessage, { reply_to_message_id: ctx.message.message_id });
|
|
832
835
|
queueItem.messageInfo = { chatId: queuedMessage.chat.id, messageId: queuedMessage.message_id };
|
|
@@ -99,7 +99,7 @@ function executeWithCommand(startScreenCmd, command, args, verbose = false) {
|
|
|
99
99
|
*/
|
|
100
100
|
export function buildExecuteAndUpdateMessage(deps) {
|
|
101
101
|
const { resolveIsolation, ISOLATION_BACKEND, isolationRunner, VERBOSE, executeStartScreen, trackSession, untrackSession, AUTO_WATCH_MESSAGE, startAutoTerminalWatchForSession, bot, formatExecutingWorkSessionMessage, formatStartingWorkSessionMessage } = deps;
|
|
102
|
-
return async function executeAndUpdateMessage(ctx, startingMessage, commandName, args, infoBlock, perCommandIsolation = null, tool = 'claude', urlContext = null, { showLimits = false, limitsAtStart = null, locale = null } = {}) {
|
|
102
|
+
return async function executeAndUpdateMessage(ctx, startingMessage, commandName, args, infoBlock, perCommandIsolation = null, tool = 'claude', urlContext = null, { showLimits = false, limitsAtStart = null, locale = null, commandAlias = null } = {}) {
|
|
103
103
|
const { chat, message_id: msgId } = startingMessage;
|
|
104
104
|
const safeEdit = async text => {
|
|
105
105
|
try {
|
|
@@ -111,7 +111,7 @@ export function buildExecuteAndUpdateMessage(deps) {
|
|
|
111
111
|
const requesterUserId = ctx.from?.id ?? null; // Issue #1688: suppress duplicate /subscribe DM
|
|
112
112
|
// #1927 review follow-up: persist the full args so a killed /solve can be
|
|
113
113
|
// resumed with its exact original invocation + `--resume <lastSessionId>`.
|
|
114
|
-
const baseSessionInfo = { chatId: ctx.chat.id, messageId: msgId, startTime: new Date(), url: args[0], command: commandName, tool, infoBlock, urlContext, requesterUserId, showLimits, limitsAtStart, locale, args: Array.isArray(args) ? [...args] : undefined }; // #594: showLimits/limitsAtStart
|
|
114
|
+
const baseSessionInfo = { chatId: ctx.chat.id, messageId: msgId, startTime: new Date(), url: args[0], command: commandName, commandAlias, tool, infoBlock, urlContext, requesterUserId, showLimits, limitsAtStart, locale, args: Array.isArray(args) ? [...args] : undefined }; // #594: showLimits/limitsAtStart
|
|
115
115
|
const iso = await resolveIsolation(perCommandIsolation, ISOLATION_BACKEND, isolationRunner, VERBOSE);
|
|
116
116
|
let result, session, sessionInfo;
|
|
117
117
|
if (iso) {
|