pikiclaw 0.2.35

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.
@@ -0,0 +1,259 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { spawn } from 'node:child_process';
5
+ export const PROCESS_RESTART_EXIT_CODE = 75;
6
+ export const PROCESS_RESTART_STATE_FILE_ENV = 'PIKICLAW_RESTART_STATE_FILE';
7
+ const runtimes = new Map();
8
+ let nextRuntimeId = 1;
9
+ let restartInFlight = false;
10
+ export function shellSplit(str) {
11
+ const args = [];
12
+ let cur = '';
13
+ let inSingle = false;
14
+ let inDouble = false;
15
+ for (const ch of str) {
16
+ if (ch === '\'' && !inDouble) {
17
+ inSingle = !inSingle;
18
+ continue;
19
+ }
20
+ if (ch === '"' && !inSingle) {
21
+ inDouble = !inDouble;
22
+ continue;
23
+ }
24
+ if (ch === ' ' && !inSingle && !inDouble) {
25
+ if (cur)
26
+ args.push(cur);
27
+ cur = '';
28
+ continue;
29
+ }
30
+ cur += ch;
31
+ }
32
+ if (cur)
33
+ args.push(cur);
34
+ return args;
35
+ }
36
+ function isNpxBinary(bin) {
37
+ return path.basename(bin, path.extname(bin)).toLowerCase() === 'npx';
38
+ }
39
+ export function ensureNonInteractiveRestartArgs(bin, args) {
40
+ if (!isNpxBinary(bin))
41
+ return args;
42
+ if (args.includes('--yes') || args.includes('-y'))
43
+ return args;
44
+ return ['--yes', ...args];
45
+ }
46
+ export function getDefaultRestartCmd() {
47
+ const argv1 = process.argv[1] ?? '';
48
+ if (argv1.endsWith('.ts') || argv1.includes('/tsx') || argv1.includes('/ts-node')) {
49
+ const isTsxLoader = !process.argv[0]?.includes('/tsx')
50
+ && process.execArgv?.some(arg => arg.includes('tsx'));
51
+ const parts = isTsxLoader ? ['tsx', argv1] : process.argv.slice(0, 2);
52
+ return parts.map(arg => arg.includes(' ') ? `"${arg}"` : arg).join(' ');
53
+ }
54
+ return 'npx --yes pikiclaw@latest';
55
+ }
56
+ export function buildRestartCommand(argv, restartCmd = process.env.PIKICLAW_RESTART_CMD || getDefaultRestartCmd()) {
57
+ const [bin, ...rawArgs] = shellSplit(restartCmd);
58
+ return {
59
+ bin,
60
+ args: [...ensureNonInteractiveRestartArgs(bin, rawArgs), ...argv],
61
+ };
62
+ }
63
+ export function registerProcessRuntime(runtime) {
64
+ const id = nextRuntimeId++;
65
+ runtimes.set(id, runtime);
66
+ return () => {
67
+ runtimes.delete(id);
68
+ };
69
+ }
70
+ export function getRegisteredRuntimeCount() {
71
+ return runtimes.size;
72
+ }
73
+ export function getActiveTaskCount() {
74
+ let total = 0;
75
+ for (const runtime of runtimes.values()) {
76
+ total += Math.max(0, runtime.getActiveTaskCount?.() || 0);
77
+ }
78
+ return total;
79
+ }
80
+ export function formatActiveTaskRestartError(activeTasks) {
81
+ return `${activeTasks} task(s) still running. Wait for them to finish or try again.`;
82
+ }
83
+ export function createRestartStateFilePath(ownerPid = process.pid) {
84
+ const dir = path.join(os.tmpdir(), 'pikiclaw');
85
+ fs.mkdirSync(dir, { recursive: true });
86
+ return path.join(dir, `restart-${ownerPid}.json`);
87
+ }
88
+ export function clearRestartStateFile(filePath) {
89
+ if (!filePath)
90
+ return;
91
+ try {
92
+ fs.unlinkSync(filePath);
93
+ }
94
+ catch { }
95
+ }
96
+ export function writeRestartStateFile(filePath, env) {
97
+ const payload = { version: 1, env };
98
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
99
+ fs.writeFileSync(filePath, JSON.stringify(payload), 'utf8');
100
+ }
101
+ export function consumeRestartStateFile(filePath) {
102
+ if (!filePath)
103
+ return {};
104
+ try {
105
+ const raw = fs.readFileSync(filePath, 'utf8');
106
+ const parsed = JSON.parse(raw);
107
+ if (parsed?.version !== 1 || !parsed.env || typeof parsed.env !== 'object')
108
+ return {};
109
+ return Object.fromEntries(Object.entries(parsed.env)
110
+ .filter((entry) => typeof entry[0] === 'string' && typeof entry[1] === 'string')
111
+ .map(([key, value]) => [key, value.trim()]));
112
+ }
113
+ catch {
114
+ return {};
115
+ }
116
+ finally {
117
+ clearRestartStateFile(filePath);
118
+ }
119
+ }
120
+ function mergeEnvValues(target, patch) {
121
+ for (const [key, rawValue] of Object.entries(patch)) {
122
+ const value = rawValue.trim();
123
+ if (!value)
124
+ continue;
125
+ if (!target[key]) {
126
+ target[key] = value;
127
+ continue;
128
+ }
129
+ const merged = new Set([
130
+ ...target[key].split(',').map(item => item.trim()).filter(Boolean),
131
+ ...value.split(',').map(item => item.trim()).filter(Boolean),
132
+ ]);
133
+ target[key] = [...merged].join(',');
134
+ }
135
+ }
136
+ function collectRestartEnv() {
137
+ const env = {};
138
+ for (const runtime of runtimes.values()) {
139
+ const patch = runtime.buildRestartEnv?.() || {};
140
+ mergeEnvValues(env, patch);
141
+ }
142
+ return env;
143
+ }
144
+ async function prepareRuntimesForRestart(log) {
145
+ for (const runtime of [...runtimes.values()]) {
146
+ const label = runtime.label ? `${runtime.label}: ` : '';
147
+ try {
148
+ await runtime.prepareForRestart?.();
149
+ }
150
+ catch (err) {
151
+ const message = err instanceof Error ? err.message : String(err);
152
+ log?.(`restart cleanup failed (${label}${message})`);
153
+ }
154
+ }
155
+ }
156
+ function buildRestartEnvForSpawn(extraEnv) {
157
+ const env = {
158
+ ...process.env,
159
+ ...extraEnv,
160
+ npm_config_yes: process.env.npm_config_yes || 'true',
161
+ };
162
+ delete env.PIKICLAW_DAEMON_CHILD;
163
+ delete env[PROCESS_RESTART_STATE_FILE_ENV];
164
+ return env;
165
+ }
166
+ function spawnReplacementProcess(bin, args, env, log) {
167
+ const child = spawn(bin, args, {
168
+ stdio: 'inherit',
169
+ detached: true,
170
+ env,
171
+ });
172
+ child.unref();
173
+ log?.(`restart: new process spawned (PID ${child.pid})`);
174
+ return child;
175
+ }
176
+ export async function requestProcessRestart(opts = {}) {
177
+ const activeTasks = getActiveTaskCount();
178
+ if (activeTasks > 0) {
179
+ return {
180
+ ok: false,
181
+ restarting: false,
182
+ error: formatActiveTaskRestartError(activeTasks),
183
+ activeTasks,
184
+ };
185
+ }
186
+ if (restartInFlight) {
187
+ return {
188
+ ok: true,
189
+ restarting: true,
190
+ error: null,
191
+ activeTasks: 0,
192
+ };
193
+ }
194
+ restartInFlight = true;
195
+ const log = opts.log;
196
+ const exit = opts.exit || process.exit;
197
+ try {
198
+ const extraEnv = collectRestartEnv();
199
+ await prepareRuntimesForRestart(log);
200
+ if (process.env.PIKICLAW_DAEMON_CHILD === '1') {
201
+ const restartStateFile = process.env[PROCESS_RESTART_STATE_FILE_ENV];
202
+ if (restartStateFile) {
203
+ if (Object.keys(extraEnv).length)
204
+ writeRestartStateFile(restartStateFile, extraEnv);
205
+ else
206
+ clearRestartStateFile(restartStateFile);
207
+ }
208
+ log?.('restart: handing off to daemon supervisor');
209
+ exit(PROCESS_RESTART_EXIT_CODE);
210
+ return { ok: true, restarting: true, error: null, activeTasks: 0 };
211
+ }
212
+ const { bin, args } = buildRestartCommand(opts.argv || process.argv.slice(2), opts.restartCmd);
213
+ log?.(`restart: spawning \`${bin} ${args.join(' ')}\``);
214
+ spawnReplacementProcess(bin, args, buildRestartEnvForSpawn(extraEnv), log);
215
+ exit(0);
216
+ return { ok: true, restarting: true, error: null, activeTasks: 0 };
217
+ }
218
+ catch (err) {
219
+ restartInFlight = false;
220
+ return {
221
+ ok: false,
222
+ restarting: false,
223
+ error: err instanceof Error ? err.message : String(err),
224
+ activeTasks: 0,
225
+ };
226
+ }
227
+ }
228
+ export function terminateProcessTree(target, opts = {}) {
229
+ const pid = typeof target === 'number' ? target : target?.pid;
230
+ if (!pid || pid <= 0)
231
+ return;
232
+ const signal = opts.signal ?? 'SIGTERM';
233
+ const forceSignal = opts.forceSignal ?? null;
234
+ const forceAfterMs = opts.forceAfterMs ?? 0;
235
+ const killPid = (targetPid, nextSignal) => {
236
+ try {
237
+ if (process.platform === 'win32') {
238
+ const args = ['/pid', String(targetPid), '/t'];
239
+ if (nextSignal === 'SIGKILL')
240
+ args.push('/f');
241
+ const killer = spawn('taskkill', args, { stdio: 'ignore', windowsHide: true });
242
+ killer.unref();
243
+ return;
244
+ }
245
+ process.kill(-targetPid, nextSignal);
246
+ }
247
+ catch {
248
+ try {
249
+ process.kill(targetPid, nextSignal);
250
+ }
251
+ catch { }
252
+ }
253
+ };
254
+ killPid(pid, signal);
255
+ if (forceSignal == null || forceAfterMs <= 0 || forceSignal === signal)
256
+ return;
257
+ const timer = setTimeout(() => killPid(pid, forceSignal), forceAfterMs);
258
+ timer.unref?.();
259
+ }
package/dist/run.js ADDED
@@ -0,0 +1,275 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * run.ts — Standalone CLI commands for pikiclaw.
4
+ *
5
+ * Usage:
6
+ * npm run command -- status
7
+ * npm run command -- claude-models
8
+ * npm run command -- codex-models
9
+ */
10
+ import { formatThinkingForDisplay } from './bot.js';
11
+ import { initializeProjectSkills, listAgents, listModels, listSkills, getUsage, doStream, getSessions, getSessionTail } from './code-agent.js';
12
+ import { loadUserConfig, resolveUserWorkdir } from './user-config.js';
13
+ function parseArgs(argv) {
14
+ const args = {
15
+ command: null, model: null, workdir: null, prompt: null, timeout: 1800, help: false,
16
+ session: null, n: 4,
17
+ };
18
+ const positional = [];
19
+ const it = argv[Symbol.iterator]();
20
+ for (const arg of it) {
21
+ switch (arg) {
22
+ case '-m':
23
+ case '--model':
24
+ args.model = it.next().value;
25
+ break;
26
+ case '-w':
27
+ case '--workdir':
28
+ args.workdir = it.next().value;
29
+ break;
30
+ case '-p':
31
+ case '--prompt':
32
+ args.prompt = it.next().value;
33
+ break;
34
+ case '-s':
35
+ case '--session':
36
+ args.session = it.next().value;
37
+ break;
38
+ case '-n':
39
+ args.n = parseInt(it.next().value ?? '', 10) || 4;
40
+ break;
41
+ case '--timeout':
42
+ args.timeout = parseInt(it.next().value ?? '', 10) || 1800;
43
+ break;
44
+ case '-h':
45
+ case '--help':
46
+ args.help = true;
47
+ break;
48
+ default:
49
+ if (arg.startsWith('-')) {
50
+ process.stderr.write(`Unknown option: ${arg}\n`);
51
+ process.exit(1);
52
+ }
53
+ else
54
+ positional.push(arg);
55
+ }
56
+ }
57
+ args.command = positional[0] ?? null;
58
+ // If no -p flag, treat remaining positional args as the prompt
59
+ if (!args.prompt && positional.length > 1)
60
+ args.prompt = positional.slice(1).join(' ');
61
+ return args;
62
+ }
63
+ const HELP = `pikiclaw run — standalone commands
64
+
65
+ Usage:
66
+ npm run command -- <command> [options]
67
+
68
+ Commands:
69
+ skills List project-defined custom skills (.pikiclaw/skills)
70
+ claude-run Run a single Claude prompt and print the result
71
+ codex-run Run a single Codex prompt and print the result
72
+ claude-status Show Claude agent info and API usage
73
+ codex-status Show Codex agent info and API usage
74
+ claude-models List available Claude models
75
+ codex-models List available Codex models
76
+ claude-sessions List recent Claude sessions for the workdir
77
+ codex-sessions List recent Codex sessions for the workdir
78
+ claude-tail Show last N messages of a Claude session
79
+ codex-tail Show last N messages of a Codex session
80
+
81
+ Options:
82
+ -p, --prompt <text> Prompt text (or pass after command as positional args)
83
+ -m, --model <model> Model to use / highlight
84
+ -w, --workdir <dir> Working directory [default: current process cwd]
85
+ -s, --session <id> Session ID (for tail; omit to use latest session)
86
+ -n <count> Number of messages to show [default: 4]
87
+ --timeout <seconds> Max seconds per request [default: 1800]
88
+ -h, --help Print this help
89
+
90
+ Examples:
91
+ npm run command -- claude-run -p "Hello world"
92
+ npm run command -- codex-run -m o3 "Explain this repo"
93
+ npm run command -- claude-run -m sonnet --timeout 60 -p "What is 1+1?"
94
+ npm run command -- claude-tail
95
+ npm run command -- claude-tail -n 10 -s <session-id>
96
+ `;
97
+ async function main() {
98
+ const args = parseArgs(process.argv.slice(2));
99
+ const userConfig = loadUserConfig();
100
+ const workdir = resolveUserWorkdir({ workdir: args.workdir, config: userConfig });
101
+ initializeProjectSkills(workdir);
102
+ if (args.help || !args.command) {
103
+ process.stdout.write(HELP);
104
+ process.exit(0);
105
+ }
106
+ switch (args.command) {
107
+ case 'skills': {
108
+ const result = listSkills(workdir);
109
+ if (!result.skills.length) {
110
+ process.stdout.write(`No custom skills found in ${workdir} (.pikiclaw/skills, .claude/commands)\n`);
111
+ break;
112
+ }
113
+ process.stdout.write(`Project skills (${result.skills.length}):\n\n`);
114
+ for (const sk of result.skills) {
115
+ const src = sk.source === 'skills' ? 'skill' : 'command';
116
+ const desc = sk.description ? ` ${sk.description}` : '';
117
+ process.stdout.write(` ${sk.name} [${src}]${desc}\n`);
118
+ }
119
+ break;
120
+ }
121
+ case 'claude-status': {
122
+ const info = listAgents({ includeVersion: true }).agents.find(a => a.agent === 'claude');
123
+ const mark = info.installed ? '\u2713' : '\u2717';
124
+ process.stdout.write(`${mark} claude ${info.version ?? 'not installed'} ${info.path ?? ''}\n`);
125
+ const usage = getUsage({ agent: 'claude' });
126
+ if (usage.error) {
127
+ process.stdout.write(` ${usage.error}\n`);
128
+ }
129
+ else {
130
+ for (const w of usage.windows) {
131
+ process.stdout.write(` [${w.label}] ${w.usedPercent ?? '?'}% used status=${w.status ?? 'n/a'}\n`);
132
+ }
133
+ }
134
+ break;
135
+ }
136
+ case 'codex-status': {
137
+ const info = listAgents({ includeVersion: true }).agents.find(a => a.agent === 'codex');
138
+ const mark = info.installed ? '\u2713' : '\u2717';
139
+ process.stdout.write(`${mark} codex ${info.version ?? 'not installed'} ${info.path ?? ''}\n`);
140
+ const usage = getUsage({ agent: 'codex' });
141
+ if (usage.error) {
142
+ process.stdout.write(` ${usage.error}\n`);
143
+ }
144
+ else {
145
+ for (const w of usage.windows) {
146
+ process.stdout.write(` [${w.label}] ${w.usedPercent ?? '?'}% used status=${w.status ?? 'n/a'}\n`);
147
+ }
148
+ }
149
+ break;
150
+ }
151
+ case 'claude-models': {
152
+ const result = await listModels('claude', { workdir, currentModel: args.model });
153
+ process.stdout.write(`Claude models${result.note ? ` (${result.note})` : ''}:\n`);
154
+ for (const m of result.models) {
155
+ process.stdout.write(` ${m.id}${m.alias ? ` (${m.alias})` : ''}\n`);
156
+ }
157
+ break;
158
+ }
159
+ case 'codex-models': {
160
+ const result = await listModels('codex', { workdir, currentModel: args.model });
161
+ process.stdout.write(`Codex models${result.note ? ` (${result.note})` : ''}:\n`);
162
+ for (const m of result.models) {
163
+ process.stdout.write(` ${m.id}${m.alias ? ` (${m.alias})` : ''}\n`);
164
+ }
165
+ break;
166
+ }
167
+ case 'claude-sessions':
168
+ case 'codex-sessions': {
169
+ const agent = args.command === 'codex-sessions' ? 'codex' : 'claude';
170
+ const limit = 20;
171
+ const result = await getSessions({ agent, workdir, limit });
172
+ if (!result.ok) {
173
+ process.stderr.write(`Error: ${result.error}\n`);
174
+ process.exit(1);
175
+ }
176
+ if (!result.sessions.length) {
177
+ process.stdout.write(`No ${agent} sessions found for ${workdir}\n`);
178
+ break;
179
+ }
180
+ process.stdout.write(`${agent} sessions (${result.sessions.length}) for ${workdir}:\n\n`);
181
+ for (const s of result.sessions) {
182
+ const run = s.running ? ' [RUNNING]' : '';
183
+ const date = s.createdAt ? s.createdAt.replace('T', ' ').slice(0, 19) : '?';
184
+ const model = s.model ? ` model=${s.model}` : '';
185
+ const title = s.title ? ` ${s.title}` : '';
186
+ const displayId = s.sessionId || '(none)';
187
+ process.stdout.write(` ${displayId} ${date}${model}${run}\n`);
188
+ if (title)
189
+ process.stdout.write(` ${title}\n`);
190
+ }
191
+ process.exit(0);
192
+ }
193
+ case 'claude-tail':
194
+ case 'codex-tail': {
195
+ const agent = args.command === 'codex-tail' ? 'codex' : 'claude';
196
+ let sessionId = args.session;
197
+ // Default: find the latest session
198
+ if (!sessionId) {
199
+ const sessions = await getSessions({ agent, workdir, limit: 1 });
200
+ if (!sessions.ok || !sessions.sessions.length) {
201
+ process.stderr.write(`No ${agent} sessions found for ${workdir}\n`);
202
+ process.exit(1);
203
+ }
204
+ sessionId = sessions.sessions[0].sessionId;
205
+ if (!sessionId) {
206
+ process.stderr.write(`Latest ${agent} session has no usable session ID\n`);
207
+ process.exit(1);
208
+ }
209
+ }
210
+ const tail = await getSessionTail({ agent, sessionId, workdir, limit: args.n });
211
+ if (!tail.ok) {
212
+ process.stderr.write(`Error: ${tail.error}\n`);
213
+ process.exit(1);
214
+ }
215
+ if (!tail.messages.length) {
216
+ process.stdout.write(`No messages found in session ${sessionId}\n`);
217
+ break;
218
+ }
219
+ process.stdout.write(`${agent} session ${sessionId.slice(0, 16)} (last ${tail.messages.length} messages)\n\n`);
220
+ for (const m of tail.messages) {
221
+ const icon = m.role === 'user' ? '👤 User' : '🤖 Assistant';
222
+ const preview = m.text.length > 300 ? m.text.slice(0, 300) + '...' : m.text;
223
+ process.stdout.write(`${icon}:\n${preview}\n\n`);
224
+ }
225
+ process.exit(0);
226
+ }
227
+ case 'claude-run':
228
+ case 'codex-run': {
229
+ const agent = args.command === 'codex-run' ? 'codex' : 'claude';
230
+ const prompt = args.prompt;
231
+ if (!prompt) {
232
+ process.stderr.write(`Missing prompt. Use -p "..." or pass text after the command.\n`);
233
+ process.exit(1);
234
+ }
235
+ const opts = {
236
+ agent, prompt, workdir, timeout: args.timeout,
237
+ sessionId: null, model: null, thinkingEffort: 'max',
238
+ onText: (text, _thinking) => {
239
+ process.stdout.write(`\r\x1b[K${text.slice(-120)}`);
240
+ },
241
+ claudeModel: agent === 'claude' ? (args.model || undefined) : undefined,
242
+ claudePermissionMode: agent === 'claude' ? 'bypassPermissions' : undefined,
243
+ codexModel: agent === 'codex' ? (args.model || undefined) : undefined,
244
+ codexFullAccess: agent === 'codex' ? true : undefined,
245
+ };
246
+ process.stdout.write(`Running ${agent}${args.model ? ` (model: ${args.model})` : ''}...\n`);
247
+ const result = await doStream(opts);
248
+ // Clear the streaming line and print final result
249
+ process.stdout.write('\r\x1b[K');
250
+ process.stdout.write(`--- ${agent} result ---\n`);
251
+ process.stdout.write(`ok: ${result.ok}\n`);
252
+ process.stdout.write(`model: ${result.model ?? '(unknown)'}\n`);
253
+ process.stdout.write(`session: ${result.sessionId ?? '(none)'}\n`);
254
+ process.stdout.write(`elapsed: ${result.elapsedS.toFixed(1)}s\n`);
255
+ process.stdout.write(`tokens: in=${result.inputTokens ?? '?'} out=${result.outputTokens ?? '?'} cached=${result.cachedInputTokens ?? '?'} cacheCreate=${result.cacheCreationInputTokens ?? '?'}\n`);
256
+ if (result.contextPercent != null) {
257
+ process.stdout.write(`context: ${result.contextUsedTokens}/${result.contextWindow} (${result.contextPercent}%)\n`);
258
+ }
259
+ process.stdout.write(`stop: ${result.stopReason ?? 'n/a'}\n`);
260
+ if (result.error)
261
+ process.stdout.write(`error: ${result.error}\n`);
262
+ process.stdout.write(`---\n`);
263
+ if (result.thinking) {
264
+ process.stdout.write(`\n<thinking>\n${formatThinkingForDisplay(result.thinking, 800)}\n</thinking>\n`);
265
+ }
266
+ process.stdout.write(`\n${result.message}\n`);
267
+ process.exit(result.ok ? 0 : 1);
268
+ }
269
+ default:
270
+ process.stderr.write(`Unknown command: ${args.command}\n`);
271
+ process.stderr.write(`Available commands: skills, claude-run, codex-run, claude-status, codex-status, claude-models, codex-models, claude-sessions, codex-sessions, claude-tail, codex-tail\n`);
272
+ process.exit(1);
273
+ }
274
+ }
275
+ main().catch(err => { console.error(err); process.exit(1); });
@@ -0,0 +1,43 @@
1
+ function getSessionRuntime(bot, session) {
2
+ const sessionId = session.sessionId || null;
3
+ if (!sessionId)
4
+ return null;
5
+ return bot.sessionStates.get(`${session.agent}:${sessionId}`) || null;
6
+ }
7
+ export function getSessionStatusForChat(bot, chat, session) {
8
+ const runtime = getSessionRuntime(bot, session);
9
+ const sessionId = session.sessionId || null;
10
+ const isCurrent = !!sessionId && (runtime
11
+ ? chat.activeSessionKey === runtime.key
12
+ : chat.agent === session.agent && chat.sessionId === sessionId);
13
+ return {
14
+ runtime,
15
+ isCurrent,
16
+ isRunning: !!runtime?.runningTaskIds.size || !!session.running,
17
+ };
18
+ }
19
+ export function getSessionStatusForBot(bot, session) {
20
+ const runtime = getSessionRuntime(bot, session);
21
+ const sessionId = session.sessionId || null;
22
+ let isCurrent = false;
23
+ if (sessionId) {
24
+ for (const [, chat] of bot.chats) {
25
+ if (runtime) {
26
+ if (chat.activeSessionKey === runtime.key) {
27
+ isCurrent = true;
28
+ break;
29
+ }
30
+ continue;
31
+ }
32
+ if (chat.agent === session.agent && chat.sessionId === sessionId) {
33
+ isCurrent = true;
34
+ break;
35
+ }
36
+ }
37
+ }
38
+ return {
39
+ runtime,
40
+ isCurrent,
41
+ isRunning: !!runtime?.runningTaskIds.size || !!session.running,
42
+ };
43
+ }