night-orch 0.3.1 → 0.3.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.
@@ -0,0 +1,88 @@
1
+ import type { Config } from '../config/schema.js';
2
+ export type InteractiveAgentType = 'claude' | 'codex';
3
+ export type InteractiveAgentSessionStatus = 'idle' | 'running' | 'failed' | 'closed';
4
+ export type InteractiveAgentSessionEventType = 'status' | 'stdout' | 'stderr' | 'text' | 'tool_call';
5
+ export interface InteractiveAgentProfileSummary {
6
+ name: string;
7
+ type: InteractiveAgentType;
8
+ command: string;
9
+ args: string[];
10
+ }
11
+ export interface InteractiveAgentSessionSummary {
12
+ id: string;
13
+ agent: InteractiveAgentType;
14
+ profileName: string | null;
15
+ status: InteractiveAgentSessionStatus;
16
+ cwd: string;
17
+ createdAt: string;
18
+ updatedAt: string;
19
+ turnCount: number;
20
+ lastError: string | null;
21
+ }
22
+ export interface InteractiveAgentSessionDetail extends InteractiveAgentSessionSummary {
23
+ continueSessionId: string | null;
24
+ runningTurnId: string | null;
25
+ }
26
+ export interface InteractiveAgentSessionEvent {
27
+ id: number;
28
+ sessionId: string;
29
+ timestamp: string;
30
+ type: InteractiveAgentSessionEventType;
31
+ data: Record<string, unknown>;
32
+ }
33
+ export interface InteractiveAgentSessionList {
34
+ generatedAt: string;
35
+ workspacePath: string;
36
+ profiles: InteractiveAgentProfileSummary[];
37
+ sessions: InteractiveAgentSessionSummary[];
38
+ }
39
+ export interface InteractiveAgentSessionEventList {
40
+ sessionId: string;
41
+ status: InteractiveAgentSessionStatus;
42
+ events: InteractiveAgentSessionEvent[];
43
+ lastEventId: number;
44
+ }
45
+ interface CreateSessionInput {
46
+ agent: InteractiveAgentType;
47
+ profileName?: string | null;
48
+ cwd?: string | null;
49
+ }
50
+ interface SessionEventListener {
51
+ (sessionId: string): void;
52
+ }
53
+ interface ManagerOptions {
54
+ workspacePath?: string;
55
+ maxEventsPerSession?: number;
56
+ }
57
+ export declare class InteractiveAgentSessionManager {
58
+ private readonly config;
59
+ private readonly sessions;
60
+ private readonly listeners;
61
+ private readonly workspacePath;
62
+ private readonly maxEventsPerSession;
63
+ private nextEventId;
64
+ constructor(config: Config, options?: ManagerOptions);
65
+ listSessions(): InteractiveAgentSessionList;
66
+ listProfiles(): InteractiveAgentProfileSummary[];
67
+ createSession(input: CreateSessionInput): InteractiveAgentSessionDetail;
68
+ getSession(sessionId: string): InteractiveAgentSessionDetail | null;
69
+ closeSession(sessionId: string): InteractiveAgentSessionDetail;
70
+ getEvents(sessionId: string, since?: number, limit?: number): InteractiveAgentSessionEventList;
71
+ sendPrompt(sessionId: string, prompt: string): {
72
+ accepted: true;
73
+ sessionId: string;
74
+ turnId: string;
75
+ };
76
+ onSessionEvent(listener: SessionEventListener): () => void;
77
+ private runTurn;
78
+ private handleStdoutLine;
79
+ private appendEvent;
80
+ private notify;
81
+ private resolveProfile;
82
+ private resolveSessionCwd;
83
+ private requireSession;
84
+ private toSummary;
85
+ private toDetail;
86
+ }
87
+ export {};
88
+ //# sourceMappingURL=agent-session.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-session.d.ts","sourceRoot":"","sources":["../../src/web/agent-session.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,567 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { mkdir, readFile, unlink } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { join, resolve, sep } from 'node:path';
5
+ import { logger } from '../utils/logger.js';
6
+ import { nowUtcIso } from '../utils/time.js';
7
+ import { buildWorkerCommand } from '../workers/command.js';
8
+ import { buildWorkerEnv } from '../workers/env.js';
9
+ import { isRecord, summarizeValue } from '../workers/events.js';
10
+ import { streamingExec } from '../workers/streaming-exec.js';
11
+ const DEFAULT_MAX_EVENTS_PER_SESSION = 2_000;
12
+ export class InteractiveAgentSessionManager {
13
+ config;
14
+ sessions = new Map();
15
+ listeners = new Set();
16
+ workspacePath;
17
+ maxEventsPerSession;
18
+ nextEventId = 1;
19
+ constructor(config, options = {}) {
20
+ this.config = config;
21
+ this.workspacePath = resolve(options.workspacePath ?? process.cwd());
22
+ this.maxEventsPerSession = clampInt(options.maxEventsPerSession ?? DEFAULT_MAX_EVENTS_PER_SESSION, 100, 20_000);
23
+ }
24
+ listSessions() {
25
+ const sessions = [...this.sessions.values()]
26
+ .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
27
+ .map((session) => this.toSummary(session));
28
+ return {
29
+ generatedAt: nowUtcIso(),
30
+ workspacePath: this.workspacePath,
31
+ profiles: this.listProfiles(),
32
+ sessions,
33
+ };
34
+ }
35
+ listProfiles() {
36
+ const out = [];
37
+ for (const [name, profile] of Object.entries(this.config.workerProfiles)) {
38
+ if (profile.type !== 'claude' && profile.type !== 'codex')
39
+ continue;
40
+ out.push({
41
+ name,
42
+ type: profile.type,
43
+ command: profile.command,
44
+ args: [...profile.args],
45
+ });
46
+ }
47
+ return out.sort((a, b) => a.name.localeCompare(b.name));
48
+ }
49
+ createSession(input) {
50
+ const profileSelection = this.resolveProfile(input.agent, input.profileName ?? null);
51
+ const cwd = this.resolveSessionCwd(input.cwd ?? null);
52
+ const now = nowUtcIso();
53
+ const id = randomUUID();
54
+ const session = {
55
+ id,
56
+ agent: input.agent,
57
+ profileName: profileSelection.profileName,
58
+ profile: profileSelection.profile,
59
+ status: 'idle',
60
+ cwd,
61
+ createdAt: now,
62
+ updatedAt: now,
63
+ turnCount: 0,
64
+ lastError: null,
65
+ continueSessionId: null,
66
+ runningTurnId: null,
67
+ events: [],
68
+ inFlight: null,
69
+ };
70
+ this.sessions.set(id, session);
71
+ this.appendEvent(session, 'status', {
72
+ message: `Created ${input.agent} interactive session`,
73
+ profile: profileSelection.profileName ?? '(fallback)',
74
+ cwd,
75
+ });
76
+ return this.toDetail(session);
77
+ }
78
+ getSession(sessionId) {
79
+ const session = this.sessions.get(sessionId);
80
+ if (!session)
81
+ return null;
82
+ return this.toDetail(session);
83
+ }
84
+ closeSession(sessionId) {
85
+ const session = this.requireSession(sessionId);
86
+ if (session.status === 'running') {
87
+ throw new Error('Session is currently running and cannot be closed');
88
+ }
89
+ if (session.status === 'closed') {
90
+ return this.toDetail(session);
91
+ }
92
+ session.status = 'closed';
93
+ session.updatedAt = nowUtcIso();
94
+ session.runningTurnId = null;
95
+ this.appendEvent(session, 'status', { message: 'Session closed' });
96
+ return this.toDetail(session);
97
+ }
98
+ getEvents(sessionId, since = 0, limit = 200) {
99
+ const session = this.requireSession(sessionId);
100
+ const boundedSince = Math.max(0, Math.floor(since));
101
+ const boundedLimit = clampInt(limit, 1, 400);
102
+ const events = session.events
103
+ .filter((event) => event.id > boundedSince)
104
+ .slice(0, boundedLimit);
105
+ return {
106
+ sessionId,
107
+ status: session.status,
108
+ events,
109
+ lastEventId: events.length > 0 ? events[events.length - 1].id : boundedSince,
110
+ };
111
+ }
112
+ sendPrompt(sessionId, prompt) {
113
+ const session = this.requireSession(sessionId);
114
+ const cleanedPrompt = prompt.trim();
115
+ if (!cleanedPrompt) {
116
+ throw new Error('prompt is required');
117
+ }
118
+ if (session.status === 'closed') {
119
+ throw new Error('Session is closed');
120
+ }
121
+ if (session.inFlight) {
122
+ throw new Error('Session already has a running prompt');
123
+ }
124
+ const turnId = randomUUID();
125
+ session.status = 'running';
126
+ session.updatedAt = nowUtcIso();
127
+ session.turnCount += 1;
128
+ session.runningTurnId = turnId;
129
+ session.lastError = null;
130
+ this.appendEvent(session, 'status', {
131
+ message: 'Turn started',
132
+ turnId,
133
+ turnCount: session.turnCount,
134
+ });
135
+ const runPromise = this.runTurn(session, turnId, cleanedPrompt);
136
+ session.inFlight = runPromise;
137
+ void runPromise
138
+ .catch((err) => {
139
+ const message = summarizeValue(err.message ?? String(err), 1_000);
140
+ session.status = 'failed';
141
+ session.lastError = `Unexpected turn failure: ${message}`;
142
+ session.runningTurnId = null;
143
+ session.updatedAt = nowUtcIso();
144
+ this.appendEvent(session, 'status', {
145
+ message: session.lastError,
146
+ turnId,
147
+ });
148
+ })
149
+ .finally(() => {
150
+ if (session.inFlight === runPromise) {
151
+ session.inFlight = null;
152
+ }
153
+ });
154
+ return { accepted: true, sessionId, turnId };
155
+ }
156
+ onSessionEvent(listener) {
157
+ this.listeners.add(listener);
158
+ return () => {
159
+ this.listeners.delete(listener);
160
+ };
161
+ }
162
+ async runTurn(session, turnId, prompt) {
163
+ const tempOutputDir = join(tmpdir(), 'night-orch-codex-output');
164
+ const tempOutputFile = session.agent === 'codex'
165
+ ? join(tempOutputDir, `interactive-${randomUUID()}.txt`)
166
+ : null;
167
+ try {
168
+ if (tempOutputFile) {
169
+ await mkdir(tempOutputDir, { recursive: true });
170
+ }
171
+ const taskArgs = buildInteractiveTaskArgs(session.agent, session.profile, session.continueSessionId, tempOutputFile);
172
+ const workerCommand = buildWorkerCommand(session.profile, taskArgs);
173
+ const env = buildWorkerEnv(session.profile);
174
+ const result = await streamingExec({
175
+ command: workerCommand.command,
176
+ args: workerCommand.args,
177
+ cwd: session.cwd,
178
+ env,
179
+ timeoutMs: session.profile.workerTimeoutSeconds * 1000,
180
+ stdin: prompt,
181
+ onStdoutLine: (line) => {
182
+ this.handleStdoutLine(session, line);
183
+ },
184
+ onStderrLine: (line) => {
185
+ this.appendEvent(session, 'stderr', { text: summarizeValue(line, 1_000) });
186
+ },
187
+ });
188
+ if (tempOutputFile) {
189
+ try {
190
+ const lastMessage = (await readFile(tempOutputFile, 'utf-8')).trim();
191
+ if (lastMessage.length > 0) {
192
+ this.appendEvent(session, 'text', {
193
+ text: summarizeValue(lastMessage, 8_000),
194
+ source: 'codex-output-last-message',
195
+ });
196
+ }
197
+ }
198
+ catch {
199
+ // Best-effort only; some codex invocations may skip output file.
200
+ }
201
+ finally {
202
+ void unlink(tempOutputFile).catch(() => { });
203
+ }
204
+ }
205
+ const sessionId = session.agent === 'codex'
206
+ ? extractCodexThreadId(result.stdout)
207
+ : extractClaudeSessionId(result.stdout);
208
+ if (sessionId) {
209
+ session.continueSessionId = sessionId;
210
+ }
211
+ if (result.outputTruncated) {
212
+ this.appendEvent(session, 'status', {
213
+ message: 'Output was truncated to tail window',
214
+ stdoutBytes: result.stdoutBytes,
215
+ stderrBytes: result.stderrBytes,
216
+ });
217
+ }
218
+ if (result.exitCode === 0 && !result.timedOut) {
219
+ session.status = 'idle';
220
+ session.lastError = null;
221
+ session.runningTurnId = null;
222
+ session.updatedAt = nowUtcIso();
223
+ this.appendEvent(session, 'status', {
224
+ message: 'Turn completed',
225
+ turnId,
226
+ exitCode: result.exitCode,
227
+ durationMs: result.durationMs,
228
+ sessionId: session.continueSessionId,
229
+ });
230
+ return;
231
+ }
232
+ const failureMessage = result.timedOut
233
+ ? `Agent timed out after ${session.profile.workerTimeoutSeconds}s`
234
+ : `Agent exited with code ${result.exitCode}`;
235
+ session.status = 'failed';
236
+ session.lastError = failureMessage;
237
+ session.runningTurnId = null;
238
+ session.updatedAt = nowUtcIso();
239
+ this.appendEvent(session, 'status', {
240
+ message: failureMessage,
241
+ turnId,
242
+ exitCode: result.exitCode,
243
+ timedOut: result.timedOut,
244
+ });
245
+ }
246
+ catch (err) {
247
+ const message = summarizeValue(err.message ?? String(err), 1_000);
248
+ session.status = 'failed';
249
+ session.lastError = message;
250
+ session.runningTurnId = null;
251
+ session.updatedAt = nowUtcIso();
252
+ this.appendEvent(session, 'status', {
253
+ message: `Turn failed: ${message}`,
254
+ turnId,
255
+ });
256
+ logger.warn({ sessionId: session.id, err }, 'Interactive agent turn failed');
257
+ }
258
+ }
259
+ handleStdoutLine(session, line) {
260
+ const parsed = tryParseJson(line);
261
+ if (!parsed) {
262
+ this.appendEvent(session, 'stdout', { text: summarizeValue(line, 1_000) });
263
+ return;
264
+ }
265
+ const handled = session.agent === 'codex'
266
+ ? emitCodexEvent(parsed, (type, data) => this.appendEvent(session, type, data))
267
+ : emitClaudeEvent(parsed, (type, data) => this.appendEvent(session, type, data));
268
+ if (!handled) {
269
+ this.appendEvent(session, 'stdout', { text: summarizeValue(line, 1_000) });
270
+ }
271
+ }
272
+ appendEvent(session, type, data) {
273
+ const event = {
274
+ id: this.nextEventId++,
275
+ sessionId: session.id,
276
+ timestamp: nowUtcIso(),
277
+ type,
278
+ data,
279
+ };
280
+ session.events.push(event);
281
+ if (session.events.length > this.maxEventsPerSession) {
282
+ session.events.splice(0, session.events.length - this.maxEventsPerSession);
283
+ }
284
+ session.updatedAt = event.timestamp;
285
+ this.notify(session.id);
286
+ }
287
+ notify(sessionId) {
288
+ for (const listener of this.listeners) {
289
+ try {
290
+ listener(sessionId);
291
+ }
292
+ catch (err) {
293
+ logger.warn({ err, sessionId }, 'Agent-session listener failed');
294
+ }
295
+ }
296
+ }
297
+ resolveProfile(agent, explicitProfileName) {
298
+ if (explicitProfileName) {
299
+ const byName = this.config.workerProfiles[explicitProfileName];
300
+ if (!byName) {
301
+ throw new Error(`Unknown profile: ${explicitProfileName}`);
302
+ }
303
+ if (byName.type !== agent) {
304
+ throw new Error(`Profile ${explicitProfileName} has type ${byName.type}; expected ${agent}`);
305
+ }
306
+ return { profileName: explicitProfileName, profile: byName };
307
+ }
308
+ for (const [name, profile] of Object.entries(this.config.workerProfiles)) {
309
+ if (profile.type === agent) {
310
+ return { profileName: name, profile };
311
+ }
312
+ }
313
+ return {
314
+ profileName: null,
315
+ profile: buildFallbackProfile(agent),
316
+ };
317
+ }
318
+ resolveSessionCwd(rawCwd) {
319
+ if (!rawCwd)
320
+ return this.workspacePath;
321
+ const resolved = resolve(this.workspacePath, rawCwd);
322
+ if (resolved !== this.workspacePath && !resolved.startsWith(this.workspacePath + sep)) {
323
+ throw new Error('cwd must be inside the night-orch workspace');
324
+ }
325
+ return resolved;
326
+ }
327
+ requireSession(sessionId) {
328
+ const session = this.sessions.get(sessionId);
329
+ if (!session) {
330
+ throw new Error(`Session not found: ${sessionId}`);
331
+ }
332
+ return session;
333
+ }
334
+ toSummary(session) {
335
+ return {
336
+ id: session.id,
337
+ agent: session.agent,
338
+ profileName: session.profileName,
339
+ status: session.status,
340
+ cwd: session.cwd,
341
+ createdAt: session.createdAt,
342
+ updatedAt: session.updatedAt,
343
+ turnCount: session.turnCount,
344
+ lastError: session.lastError,
345
+ };
346
+ }
347
+ toDetail(session) {
348
+ return {
349
+ ...this.toSummary(session),
350
+ continueSessionId: session.continueSessionId,
351
+ runningTurnId: session.runningTurnId,
352
+ };
353
+ }
354
+ }
355
+ function buildFallbackProfile(agent) {
356
+ return {
357
+ type: agent,
358
+ command: agent,
359
+ args: agent === 'codex' ? ['exec'] : ['-p'],
360
+ workerTimeoutSeconds: 1800,
361
+ minimalEnv: true,
362
+ runtimeWrapper: null,
363
+ env: {},
364
+ };
365
+ }
366
+ function buildInteractiveTaskArgs(agent, profile, continueSessionId, outputFile) {
367
+ if (agent === 'codex') {
368
+ const args = [...profile.args];
369
+ if (outputFile) {
370
+ args.push('--output-last-message', outputFile);
371
+ }
372
+ if (continueSessionId) {
373
+ const execIndex = args.indexOf('exec');
374
+ if (execIndex >= 0) {
375
+ args.splice(execIndex + 1, 0, 'resume', continueSessionId);
376
+ }
377
+ }
378
+ return args;
379
+ }
380
+ const args = [
381
+ ...profile.args,
382
+ '--output-format', 'json',
383
+ '--max-turns', '50',
384
+ ];
385
+ if (!hasExplicitPermissionMode(args)) {
386
+ args.push('--permission-mode', resolveDefaultPermissionMode());
387
+ }
388
+ if (continueSessionId) {
389
+ args.push('--continue', continueSessionId);
390
+ }
391
+ return args;
392
+ }
393
+ function hasExplicitPermissionMode(args) {
394
+ for (const arg of args) {
395
+ if (arg === '--permission-mode' || arg.startsWith('--permission-mode='))
396
+ return true;
397
+ if (arg === '--dangerously-skip-permissions')
398
+ return true;
399
+ if (arg === '--allow-dangerously-skip-permissions')
400
+ return true;
401
+ }
402
+ return false;
403
+ }
404
+ function resolveDefaultPermissionMode() {
405
+ try {
406
+ if (typeof process.getuid === 'function' && process.getuid() === 0) {
407
+ return 'acceptEdits';
408
+ }
409
+ }
410
+ catch {
411
+ // Ignore and keep non-root default.
412
+ }
413
+ return 'bypassPermissions';
414
+ }
415
+ function emitCodexEvent(parsed, emit) {
416
+ if (!isRecord(parsed))
417
+ return false;
418
+ const type = parsed['type'];
419
+ if (type === 'item.completed' && isRecord(parsed['item'])) {
420
+ const item = parsed['item'];
421
+ if (item['type'] === 'agent_message' && typeof item['text'] === 'string') {
422
+ emit('text', { text: summarizeValue(item['text'], 8_000) });
423
+ return true;
424
+ }
425
+ if ((item['type'] === 'function_call' || item['type'] === 'tool_call') && typeof item['name'] === 'string') {
426
+ emit('tool_call', {
427
+ toolName: item['name'],
428
+ toolArgs: summarizeValue(item['arguments'] ?? item['args'], 1_000),
429
+ });
430
+ return true;
431
+ }
432
+ }
433
+ if (type === 'function_call' && typeof parsed['name'] === 'string') {
434
+ emit('tool_call', {
435
+ toolName: parsed['name'],
436
+ toolArgs: summarizeValue(parsed['arguments'] ?? parsed['args'], 1_000),
437
+ });
438
+ return true;
439
+ }
440
+ if (type === 'error') {
441
+ emit('stderr', { text: summarizeValue(parsed['error'], 1_000) });
442
+ return true;
443
+ }
444
+ return false;
445
+ }
446
+ function emitClaudeEvent(parsed, emit) {
447
+ if (Array.isArray(parsed)) {
448
+ let handled = false;
449
+ for (const item of parsed) {
450
+ handled = emitClaudeEvent(item, emit) || handled;
451
+ }
452
+ return handled;
453
+ }
454
+ if (!isRecord(parsed))
455
+ return false;
456
+ const type = parsed['type'];
457
+ if (type !== 'assistant') {
458
+ if (type === 'error') {
459
+ emit('stderr', { text: summarizeValue(parsed['error'], 1_000) });
460
+ return true;
461
+ }
462
+ return false;
463
+ }
464
+ const message = parsed['message'];
465
+ if (!isRecord(message) || !Array.isArray(message['content']))
466
+ return false;
467
+ let handled = false;
468
+ for (const block of message['content']) {
469
+ if (!isRecord(block))
470
+ continue;
471
+ if (block['type'] === 'text' && typeof block['text'] === 'string') {
472
+ emit('text', { text: summarizeValue(block['text'], 8_000) });
473
+ handled = true;
474
+ continue;
475
+ }
476
+ if (block['type'] === 'tool_use') {
477
+ emit('tool_call', {
478
+ toolName: typeof block['name'] === 'string' ? block['name'] : 'tool',
479
+ toolArgs: summarizeValue(block['input'], 1_000),
480
+ });
481
+ handled = true;
482
+ }
483
+ }
484
+ return handled;
485
+ }
486
+ function tryParseJson(value) {
487
+ try {
488
+ return JSON.parse(value);
489
+ }
490
+ catch {
491
+ return null;
492
+ }
493
+ }
494
+ function extractCodexThreadId(raw) {
495
+ for (const event of parseCodexEvents(raw)) {
496
+ if (!isRecord(event))
497
+ continue;
498
+ if (isRecord(event['session']) && typeof event['session']['thread_id'] === 'string') {
499
+ return event['session']['thread_id'];
500
+ }
501
+ if (typeof event['thread_id'] === 'string') {
502
+ return event['thread_id'];
503
+ }
504
+ }
505
+ return null;
506
+ }
507
+ function parseCodexEvents(raw) {
508
+ const trimmed = raw.trim();
509
+ if (!trimmed)
510
+ return [];
511
+ if (trimmed.startsWith('[')) {
512
+ try {
513
+ const parsed = JSON.parse(trimmed);
514
+ return Array.isArray(parsed) ? parsed : [];
515
+ }
516
+ catch {
517
+ return [];
518
+ }
519
+ }
520
+ const out = [];
521
+ for (const line of trimmed.split('\n')) {
522
+ const parsed = tryParseJson(line.trim());
523
+ if (parsed)
524
+ out.push(parsed);
525
+ }
526
+ return out;
527
+ }
528
+ function extractClaudeSessionId(raw) {
529
+ const trimmed = raw.trim();
530
+ if (!trimmed)
531
+ return null;
532
+ const parsed = tryParseJson(trimmed);
533
+ if (!parsed)
534
+ return null;
535
+ if (Array.isArray(parsed)) {
536
+ for (const item of parsed) {
537
+ if (!isRecord(item))
538
+ continue;
539
+ if (item['type'] === 'system' && isRecord(item['session']) && typeof item['session']['session_id'] === 'string') {
540
+ return item['session']['session_id'];
541
+ }
542
+ if (typeof item['session_id'] === 'string') {
543
+ return item['session_id'];
544
+ }
545
+ }
546
+ return null;
547
+ }
548
+ if (isRecord(parsed)) {
549
+ if (typeof parsed['session_id'] === 'string')
550
+ return parsed['session_id'];
551
+ if (isRecord(parsed['session']) && typeof parsed['session']['session_id'] === 'string') {
552
+ return parsed['session']['session_id'];
553
+ }
554
+ }
555
+ return null;
556
+ }
557
+ function clampInt(value, min, max) {
558
+ if (!Number.isFinite(value))
559
+ return min;
560
+ const floor = Math.floor(value);
561
+ if (floor < min)
562
+ return min;
563
+ if (floor > max)
564
+ return max;
565
+ return floor;
566
+ }
567
+ //# sourceMappingURL=agent-session.js.map