@inixiative/agent-session 0.5.2 → 0.6.1
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 +45 -13
- package/package.json +3 -3
- package/src/claude-agent-sdk-session.ts +136 -3
- package/src/claude-code-session.ts +275 -14
- package/src/claude-primed.ts +153 -35
- package/src/codex-session.ts +339 -607
- package/src/harness-session.ts +52 -2
- package/src/index.ts +6 -4
- package/src/pool.ts +1 -1
- package/src/primed.ts +1 -1
- package/src/tool-gate.ts +61 -0
- package/src/transport.ts +11 -38
- package/src/transports.ts +1 -4
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { mkdtempSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
1
4
|
import {
|
|
2
5
|
claudeRateLimitSnapshot,
|
|
3
6
|
claudeUsageSnapshot,
|
|
@@ -5,6 +8,7 @@ import {
|
|
|
5
8
|
mergeLimits,
|
|
6
9
|
} from './limits';
|
|
7
10
|
import { retainEvidence } from './retained-evidence';
|
|
11
|
+
import { mcpToolParts, ToolGates } from './tool-gate';
|
|
8
12
|
import { TRANSPORTS } from './transport';
|
|
9
13
|
import { TurnState } from './turn-state';
|
|
10
14
|
|
|
@@ -53,6 +57,9 @@ import type {
|
|
|
53
57
|
SessionResult,
|
|
54
58
|
SessionSendOptions,
|
|
55
59
|
SessionTokens,
|
|
60
|
+
ToolDecision,
|
|
61
|
+
ToolGate,
|
|
62
|
+
ToolRequest,
|
|
56
63
|
} from './harness-session';
|
|
57
64
|
|
|
58
65
|
// Re-export types so existing import paths keep working
|
|
@@ -88,7 +95,8 @@ export interface ClaudeCodeSessionConfig {
|
|
|
88
95
|
/**
|
|
89
96
|
* Base context to pre-load at session startup.
|
|
90
97
|
*
|
|
91
|
-
* Injected via --append-system-prompt on process spawn
|
|
98
|
+
* Injected via --append-system-prompt-file on process spawn (a private 0600 file, removed when the
|
|
99
|
+
* process ends), so a base past Linux's 128 KiB per-argument limit still starts. Persists for the
|
|
92
100
|
* entire session lifetime. Include all stable context here (system,
|
|
93
101
|
* conventions, memory, architecture) so per-message delta is minimal.
|
|
94
102
|
*/
|
|
@@ -113,6 +121,14 @@ export interface ClaudeCodeSessionConfig {
|
|
|
113
121
|
textOnly?: boolean;
|
|
114
122
|
/** False adds `--no-session-persistence` (nothing written to history; cannot be resumed). Default true. */
|
|
115
123
|
persistSession?: boolean;
|
|
124
|
+
/**
|
|
125
|
+
* Launch for per-call tool decisions: `--permission-prompt-tool stdio` plus a PreToolUse
|
|
126
|
+
* callback registered over the control protocol at start(). Gates added with
|
|
127
|
+
* onToolRequest() decide (a `permission` ask with no gate is denied). A callback or ask that
|
|
128
|
+
* cannot be read as a tool request is denied (fail closed). A turn timeout leaves a pending
|
|
129
|
+
* ask open; interruptNative() withdraws it. Not with textOnly.
|
|
130
|
+
*/
|
|
131
|
+
toolGate?: boolean;
|
|
116
132
|
/**
|
|
117
133
|
* Override for the process spawner. Defaults to Bun.spawn. Tests inject a
|
|
118
134
|
* fake subprocess that emulates the claude CLI's stream-json protocol.
|
|
@@ -246,12 +262,19 @@ export class ClaudeCodeSession implements HarnessSession {
|
|
|
246
262
|
private _permissionMode: string;
|
|
247
263
|
private _defaultTimeout: number;
|
|
248
264
|
private _baseContext?: string;
|
|
265
|
+
/** The private directory holding the base context file while a process runs. */
|
|
266
|
+
private _baseContextDir?: string;
|
|
249
267
|
private _forking = false;
|
|
250
268
|
private _awaitingForkIdentity = false;
|
|
251
269
|
private _spawn?: ClaudeCodeSessionConfig['spawn'];
|
|
252
270
|
private _env?: ClaudeCodeSessionConfig['env'];
|
|
253
271
|
private _textOnly: boolean;
|
|
254
272
|
private _persistSession: boolean;
|
|
273
|
+
private _toolGate: boolean;
|
|
274
|
+
private _gates = new ToolGates();
|
|
275
|
+
/** Inbound control requests awaiting a gate decision, by request id. */
|
|
276
|
+
private _inbound = new Map<string, AbortController>();
|
|
277
|
+
private readonly _hookCallbackId = `agent-session-pre-tool-${crypto.randomUUID()}`;
|
|
255
278
|
private _limits?: LimitSnapshot;
|
|
256
279
|
private _controlSeq = 0;
|
|
257
280
|
private _control = new Map<
|
|
@@ -325,6 +348,9 @@ export class ClaudeCodeSession implements HarnessSession {
|
|
|
325
348
|
this._env = config?.env;
|
|
326
349
|
this._textOnly = config?.textOnly ?? false;
|
|
327
350
|
this._persistSession = config?.persistSession ?? true;
|
|
351
|
+
this._toolGate = config?.toolGate ?? false;
|
|
352
|
+
if (this._toolGate && this._textOnly)
|
|
353
|
+
throw new Error('toolGate requires a session with tools; textOnly sessions have none');
|
|
328
354
|
this._startedAt = Date.now();
|
|
329
355
|
}
|
|
330
356
|
|
|
@@ -375,6 +401,12 @@ export class ClaudeCodeSession implements HarnessSession {
|
|
|
375
401
|
};
|
|
376
402
|
}
|
|
377
403
|
|
|
404
|
+
/** Register a tool gate. A fork gets a snapshot of the gates registered at fork() time. */
|
|
405
|
+
onToolRequest(gate: ToolGate): () => void {
|
|
406
|
+
if (!this._toolGate) throw new Error('Tool gates need a session launched with toolGate: true');
|
|
407
|
+
return this._gates.add(gate);
|
|
408
|
+
}
|
|
409
|
+
|
|
378
410
|
onBeforeSend(hook: BeforeSendHook): () => void {
|
|
379
411
|
this._beforeSendHooks.push(hook);
|
|
380
412
|
return () => {
|
|
@@ -422,16 +454,21 @@ export class ClaudeCodeSession implements HarnessSession {
|
|
|
422
454
|
delete env.ANTHROPIC_AUTH_TOKEN;
|
|
423
455
|
for (const key of Object.keys(env)) if (env[key] === undefined) delete env[key];
|
|
424
456
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
457
|
+
try {
|
|
458
|
+
if (this._spawn) {
|
|
459
|
+
this._proc = this._spawn([this._bin, ...args], { cwd: this._cwd, env });
|
|
460
|
+
} else {
|
|
461
|
+
this._proc = Bun.spawn([this._bin, ...args], {
|
|
462
|
+
cwd: this._cwd,
|
|
463
|
+
stdin: 'pipe',
|
|
464
|
+
stdout: 'pipe',
|
|
465
|
+
stderr: 'pipe',
|
|
466
|
+
env,
|
|
467
|
+
}) as unknown as PipedSubprocess;
|
|
468
|
+
}
|
|
469
|
+
} catch (error) {
|
|
470
|
+
this._removeBaseContext();
|
|
471
|
+
throw error;
|
|
435
472
|
}
|
|
436
473
|
|
|
437
474
|
this._alive = true;
|
|
@@ -444,7 +481,10 @@ export class ClaudeCodeSession implements HarnessSession {
|
|
|
444
481
|
this._readStderr();
|
|
445
482
|
|
|
446
483
|
// Monitor process exit for cleanup
|
|
484
|
+
// The process read its base context file at startup; it goes when the process does.
|
|
485
|
+
const baseDir = this._baseContextDir;
|
|
447
486
|
proc.exited.then(async (code) => {
|
|
487
|
+
this._removeBaseContext(baseDir);
|
|
448
488
|
if (this._proc !== proc || !this._alive) return;
|
|
449
489
|
this._alive = false;
|
|
450
490
|
await stdoutDone; // Drain buffered terminal evidence before interpreting process exit.
|
|
@@ -456,11 +496,30 @@ export class ClaudeCodeSession implements HarnessSession {
|
|
|
456
496
|
this._rejectControl(new Error('Session ended'));
|
|
457
497
|
this._emit({ kind: 'session_end', timestamp: Date.now(), transportOutcome: 'failed' });
|
|
458
498
|
});
|
|
499
|
+
|
|
500
|
+
// The PreToolUse callback is registered before any turn is written, so it covers the first call.
|
|
501
|
+
if (this._toolGate) {
|
|
502
|
+
try {
|
|
503
|
+
await this._controlRequest(
|
|
504
|
+
{
|
|
505
|
+
subtype: 'initialize',
|
|
506
|
+
hooks: { PreToolUse: [{ matcher: '.*', hookCallbackIds: [this._hookCallbackId] }] },
|
|
507
|
+
},
|
|
508
|
+
30_000,
|
|
509
|
+
);
|
|
510
|
+
} catch (error) {
|
|
511
|
+
this.kill();
|
|
512
|
+
throw new Error(`Tool gate registration failed: ${(error as Error).message}`);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
459
515
|
}
|
|
460
516
|
|
|
461
517
|
private _rejectControl(error: Error): void {
|
|
462
518
|
for (const pending of this._control.values()) pending.reject(error);
|
|
463
519
|
this._control.clear();
|
|
520
|
+
// Decisions still pending have nowhere to go: withdraw them.
|
|
521
|
+
for (const inbound of this._inbound.values()) inbound.abort();
|
|
522
|
+
this._inbound.clear();
|
|
464
523
|
}
|
|
465
524
|
|
|
466
525
|
// ---------------------------------------------------------------------------
|
|
@@ -530,7 +589,7 @@ export class ClaudeCodeSession implements HarnessSession {
|
|
|
530
589
|
}
|
|
531
590
|
|
|
532
591
|
// ---------------------------------------------------------------------------
|
|
533
|
-
// fork() — branch from current conversation state
|
|
592
|
+
// fork() — branch from current conversation state (and a snapshot of the tool gates)
|
|
534
593
|
// ---------------------------------------------------------------------------
|
|
535
594
|
|
|
536
595
|
fork(opts?: {
|
|
@@ -558,7 +617,9 @@ export class ClaudeCodeSession implements HarnessSession {
|
|
|
558
617
|
env: this._env,
|
|
559
618
|
textOnly: this._textOnly,
|
|
560
619
|
persistSession: opts?.persistSession ?? this._persistSession,
|
|
620
|
+
toolGate: this._toolGate,
|
|
561
621
|
});
|
|
622
|
+
for (const gate of this._gates.list()) forked._gates.add(gate);
|
|
562
623
|
forked._forking = true;
|
|
563
624
|
return forked;
|
|
564
625
|
}
|
|
@@ -687,6 +748,7 @@ export class ClaudeCodeSession implements HarnessSession {
|
|
|
687
748
|
/* already dead */
|
|
688
749
|
}
|
|
689
750
|
this._proc = null;
|
|
751
|
+
this._removeBaseContext();
|
|
690
752
|
|
|
691
753
|
this._emit({ kind: 'session_end', timestamp: Date.now() });
|
|
692
754
|
}
|
|
@@ -930,9 +992,32 @@ export class ClaudeCodeSession implements HarnessSession {
|
|
|
930
992
|
}
|
|
931
993
|
return;
|
|
932
994
|
}
|
|
995
|
+
if (raw.type === 'control_cancel_request') {
|
|
996
|
+
const id = typeof raw.request_id === 'string' ? raw.request_id : undefined;
|
|
997
|
+
const inbound = id ? this._inbound.get(id) : undefined;
|
|
998
|
+
if (inbound && id) {
|
|
999
|
+
this._inbound.delete(id);
|
|
1000
|
+
inbound.abort();
|
|
1001
|
+
}
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
933
1004
|
if (raw.type === 'control_request') {
|
|
934
|
-
// No client-side handlers (can_use_tool, hooks) are registered on this transport.
|
|
935
1005
|
const id = typeof raw.request_id === 'string' ? raw.request_id : undefined;
|
|
1006
|
+
const request = raw.request as Record<string, unknown> | undefined;
|
|
1007
|
+
const toolRequest =
|
|
1008
|
+
id && this._toolGate && this._proc ? this._toolRequestFrom(request) : undefined;
|
|
1009
|
+
if (id && toolRequest) {
|
|
1010
|
+
this._answerToolRequest(this._proc!, id, toolRequest);
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
// A gated ask or our own callback that cannot be read is denied: an error reply allows.
|
|
1014
|
+
const unreadable = id && this._toolGate && this._proc ? this._unreadable(request) : undefined;
|
|
1015
|
+
if (id && unreadable) {
|
|
1016
|
+
this._writeControlResponse(this._proc!, id, unreadable);
|
|
1017
|
+
this._unattributed(raw, 'unrecognized-event');
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
// No other client-side handlers are registered on this transport.
|
|
936
1021
|
if (id && this._proc) {
|
|
937
1022
|
try {
|
|
938
1023
|
this._proc.stdin.write(
|
|
@@ -1094,6 +1179,129 @@ export class ClaudeCodeSession implements HarnessSession {
|
|
|
1094
1179
|
}
|
|
1095
1180
|
}
|
|
1096
1181
|
|
|
1182
|
+
// ---------------------------------------------------------------------------
|
|
1183
|
+
// Private — tool gate (control protocol: hook_callback, can_use_tool)
|
|
1184
|
+
// ---------------------------------------------------------------------------
|
|
1185
|
+
|
|
1186
|
+
private _toolRequestFrom(
|
|
1187
|
+
request: Record<string, unknown> | undefined,
|
|
1188
|
+
): Omit<ToolRequest, 'signal'> | undefined {
|
|
1189
|
+
const record = (value: unknown) =>
|
|
1190
|
+
value && typeof value === 'object' && !Array.isArray(value)
|
|
1191
|
+
? (value as Record<string, unknown>)
|
|
1192
|
+
: undefined;
|
|
1193
|
+
const text = (value: unknown) => (typeof value === 'string' && value ? value : undefined);
|
|
1194
|
+
let stage: ToolRequest['stage'];
|
|
1195
|
+
let source: Record<string, unknown> | undefined;
|
|
1196
|
+
let toolName: string | undefined;
|
|
1197
|
+
let toolInput: Record<string, unknown> | undefined;
|
|
1198
|
+
if (request && this._ownHookCallback(request)) {
|
|
1199
|
+
source = record(request.input);
|
|
1200
|
+
if (source?.hook_event_name !== 'PreToolUse') return undefined;
|
|
1201
|
+
stage = 'pre-use';
|
|
1202
|
+
toolName = text(source.tool_name);
|
|
1203
|
+
toolInput = record(source.tool_input);
|
|
1204
|
+
} else if (request?.subtype === 'can_use_tool') {
|
|
1205
|
+
source = request;
|
|
1206
|
+
stage = 'permission';
|
|
1207
|
+
toolName = text(request.tool_name);
|
|
1208
|
+
toolInput = record(request.input);
|
|
1209
|
+
} else return undefined;
|
|
1210
|
+
if (!toolName) return undefined;
|
|
1211
|
+
const callId = text(source?.tool_use_id) ?? text(request.tool_use_id);
|
|
1212
|
+
const agentId = text(source?.agent_id) ?? text(request.agent_id);
|
|
1213
|
+
const turn = this._inflight;
|
|
1214
|
+
return {
|
|
1215
|
+
stage,
|
|
1216
|
+
toolName,
|
|
1217
|
+
...mcpToolParts(toolName),
|
|
1218
|
+
toolInput: Object.freeze(structuredClone(toolInput ?? {})),
|
|
1219
|
+
...(callId ? { callId } : {}),
|
|
1220
|
+
...(turn?.evidence.dispatch === 'attempted' && turn.evidence.nativeOutcome === 'unknown'
|
|
1221
|
+
? { admissionId: turn.evidence.admissionId }
|
|
1222
|
+
: {}),
|
|
1223
|
+
...(text(request.description) ? { description: text(request.description) } : {}),
|
|
1224
|
+
...(text(request.decision_reason) ? { decisionReason: text(request.decision_reason) } : {}),
|
|
1225
|
+
...(text(request.blocked_path) ? { blockedPath: text(request.blocked_path) } : {}),
|
|
1226
|
+
...(agentId ? { agentId } : {}),
|
|
1227
|
+
};
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
private _ownHookCallback(request: Record<string, unknown> | undefined): boolean {
|
|
1231
|
+
return request?.subtype === 'hook_callback' && request.callback_id === this._hookCallbackId;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
/** The deny for a gated request _toolRequestFrom could not read; undefined when not ours. */
|
|
1235
|
+
private _unreadable(
|
|
1236
|
+
request: Record<string, unknown> | undefined,
|
|
1237
|
+
): Record<string, unknown> | undefined {
|
|
1238
|
+
if (this._ownHookCallback(request))
|
|
1239
|
+
return this._toolResponse(
|
|
1240
|
+
{ stage: 'pre-use', toolName: '', toolInput: {} },
|
|
1241
|
+
{ behavior: 'deny', message: 'Tool gate: unreadable PreToolUse callback' },
|
|
1242
|
+
);
|
|
1243
|
+
if (request?.subtype === 'can_use_tool')
|
|
1244
|
+
return { behavior: 'deny', message: 'Tool gate: unreadable permission request' };
|
|
1245
|
+
return undefined;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
private _answerToolRequest(
|
|
1249
|
+
proc: PipedSubprocess,
|
|
1250
|
+
requestId: string,
|
|
1251
|
+
request: Omit<ToolRequest, 'signal'>,
|
|
1252
|
+
): void {
|
|
1253
|
+
const controller = new AbortController();
|
|
1254
|
+
this._inbound.set(requestId, controller);
|
|
1255
|
+
void this._gates
|
|
1256
|
+
.decide({ ...request, signal: controller.signal })
|
|
1257
|
+
.then((decision) => {
|
|
1258
|
+
// Withdrawn by the runtime, or the process is gone: nothing may be written for it.
|
|
1259
|
+
if (controller.signal.aborted || this._proc !== proc || !this._alive) return;
|
|
1260
|
+
this._inbound.delete(requestId);
|
|
1261
|
+
this._writeControlResponse(proc, requestId, this._toolResponse(request, decision));
|
|
1262
|
+
})
|
|
1263
|
+
.finally(() => {
|
|
1264
|
+
if (this._inbound.get(requestId) === controller) this._inbound.delete(requestId);
|
|
1265
|
+
});
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
private _toolResponse(
|
|
1269
|
+
request: Omit<ToolRequest, 'signal'>,
|
|
1270
|
+
decision: ToolDecision | undefined,
|
|
1271
|
+
): Record<string, unknown> {
|
|
1272
|
+
if (request.stage === 'pre-use')
|
|
1273
|
+
return decision?.behavior === 'deny'
|
|
1274
|
+
? {
|
|
1275
|
+
hookSpecificOutput: {
|
|
1276
|
+
hookEventName: 'PreToolUse',
|
|
1277
|
+
permissionDecision: 'deny',
|
|
1278
|
+
permissionDecisionReason: decision.message,
|
|
1279
|
+
},
|
|
1280
|
+
}
|
|
1281
|
+
: {};
|
|
1282
|
+
return decision?.behavior === 'allow'
|
|
1283
|
+
? { behavior: 'allow', updatedInput: structuredClone(request.toolInput) }
|
|
1284
|
+
: { behavior: 'deny', message: decision?.message ?? 'Denied' };
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
private _writeControlResponse(
|
|
1288
|
+
proc: PipedSubprocess,
|
|
1289
|
+
requestId: string,
|
|
1290
|
+
response: Record<string, unknown>,
|
|
1291
|
+
): void {
|
|
1292
|
+
try {
|
|
1293
|
+
proc.stdin.write(
|
|
1294
|
+
`${JSON.stringify({
|
|
1295
|
+
type: 'control_response',
|
|
1296
|
+
response: { subtype: 'success', request_id: requestId, response },
|
|
1297
|
+
})}\n`,
|
|
1298
|
+
);
|
|
1299
|
+
proc.stdin.flush();
|
|
1300
|
+
} catch {
|
|
1301
|
+
/* transport failure surfaces on the stream */
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1097
1305
|
// ---------------------------------------------------------------------------
|
|
1098
1306
|
// Private — spawn args (called once at start())
|
|
1099
1307
|
// ---------------------------------------------------------------------------
|
|
@@ -1115,6 +1323,7 @@ export class ClaudeCodeSession implements HarnessSession {
|
|
|
1115
1323
|
'--permission-mode',
|
|
1116
1324
|
this._permissionMode,
|
|
1117
1325
|
'--include-hook-events',
|
|
1326
|
+
...(this._toolGate ? ['--permission-prompt-tool', 'stdio'] : []),
|
|
1118
1327
|
];
|
|
1119
1328
|
|
|
1120
1329
|
// Resume for fork or crash recovery. _externalSessionId is set from
|
|
@@ -1134,12 +1343,32 @@ export class ClaudeCodeSession implements HarnessSession {
|
|
|
1134
1343
|
|
|
1135
1344
|
// Stable base context injected once at process startup
|
|
1136
1345
|
if (this._baseContext) {
|
|
1137
|
-
args.push('--append-system-prompt', this._baseContext);
|
|
1346
|
+
args.push('--append-system-prompt-file', this._writeBaseContext(this._baseContext));
|
|
1138
1347
|
}
|
|
1139
1348
|
|
|
1140
1349
|
return args;
|
|
1141
1350
|
}
|
|
1142
1351
|
|
|
1352
|
+
/** The base context as a private file: argv carries its path, never its text. */
|
|
1353
|
+
private _writeBaseContext(text: string): string {
|
|
1354
|
+
this._removeBaseContext();
|
|
1355
|
+
if (!swept) {
|
|
1356
|
+
swept = true;
|
|
1357
|
+
sweepOrphanedBaseContexts();
|
|
1358
|
+
}
|
|
1359
|
+
this._baseContextDir = mkdtempSync(join(tmpdir(), `${BASE_CONTEXT_PREFIX}${process.pid}-`));
|
|
1360
|
+
const file = join(this._baseContextDir, 'append-system-prompt.md');
|
|
1361
|
+
writeFileSync(file, text, { mode: 0o600 });
|
|
1362
|
+
return file;
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
/** Remove a spawn's base context directory: the current one by default. */
|
|
1366
|
+
private _removeBaseContext(dir = this._baseContextDir): void {
|
|
1367
|
+
if (!dir) return;
|
|
1368
|
+
rmSync(dir, { recursive: true, force: true });
|
|
1369
|
+
if (dir === this._baseContextDir) this._baseContextDir = undefined;
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1143
1372
|
// ---------------------------------------------------------------------------
|
|
1144
1373
|
// Private — event classification
|
|
1145
1374
|
// ---------------------------------------------------------------------------
|
|
@@ -1297,3 +1526,35 @@ export class ClaudeCodeSession implements HarnessSession {
|
|
|
1297
1526
|
}
|
|
1298
1527
|
}
|
|
1299
1528
|
}
|
|
1529
|
+
|
|
1530
|
+
const BASE_CONTEXT_PREFIX = 'agent-session-base-';
|
|
1531
|
+
let swept = false;
|
|
1532
|
+
|
|
1533
|
+
/**
|
|
1534
|
+
* Remove base context files a crashed or killed process left behind (each holds a whole composed
|
|
1535
|
+
* context). Runs once per process; a directory belongs to the pid in its name and goes only when
|
|
1536
|
+
* that process is gone.
|
|
1537
|
+
*/
|
|
1538
|
+
export function sweepOrphanedBaseContexts(dir = tmpdir()): void {
|
|
1539
|
+
let entries: string[];
|
|
1540
|
+
try {
|
|
1541
|
+
entries = readdirSync(dir);
|
|
1542
|
+
} catch {
|
|
1543
|
+
return;
|
|
1544
|
+
}
|
|
1545
|
+
for (const entry of entries) {
|
|
1546
|
+
const pid = Number(new RegExp(`^${BASE_CONTEXT_PREFIX}(\\d+)-`).exec(entry)?.[1]);
|
|
1547
|
+
if (!pid || pid === process.pid || processAlive(pid)) continue;
|
|
1548
|
+
rmSync(join(dir, entry), { recursive: true, force: true });
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
function processAlive(pid: number): boolean {
|
|
1553
|
+
try {
|
|
1554
|
+
process.kill(pid, 0);
|
|
1555
|
+
return true;
|
|
1556
|
+
} catch (error) {
|
|
1557
|
+
// EPERM: it exists under another user.
|
|
1558
|
+
return (error as NodeJS.ErrnoException).code === 'EPERM';
|
|
1559
|
+
}
|
|
1560
|
+
}
|