@alfe.ai/claude-code-host 0.1.12 → 0.1.13
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/dist/host-config.cjs +108 -42
- package/dist/host-config.js +108 -42
- package/dist/index.d.cts +4 -5
- package/dist/index.d.ts +4 -5
- package/package.json +1 -1
package/dist/host-config.cjs
CHANGED
|
@@ -3,9 +3,9 @@ let _alfe_ai_chat = require("@alfe.ai/chat");
|
|
|
3
3
|
let node_fs_promises = require("node:fs/promises");
|
|
4
4
|
let node_path = require("node:path");
|
|
5
5
|
let node_os = require("node:os");
|
|
6
|
+
let node_crypto = require("node:crypto");
|
|
6
7
|
let node_child_process = require("node:child_process");
|
|
7
8
|
let node_readline = require("node:readline");
|
|
8
|
-
let node_crypto = require("node:crypto");
|
|
9
9
|
let node_fs = require("node:fs");
|
|
10
10
|
let _alfe_ai_config = require("@alfe.ai/config");
|
|
11
11
|
//#region src/child-env.ts
|
|
@@ -925,6 +925,7 @@ const MAX_IDENTIFIER_CHARS = 512;
|
|
|
925
925
|
const MAX_MEMORY_CONTEXT_CHARS = 64 * 1024;
|
|
926
926
|
const MAX_CLAUDE_SESSION_IDS = 1e3;
|
|
927
927
|
const MAX_IDEMPOTENCY_KEYS = 1e4;
|
|
928
|
+
const MAX_IDEMPOTENCY_RESULT_BYTES = 8 * 1024 * 1024;
|
|
928
929
|
const SHUTDOWN_GRACE_MS = 3e3;
|
|
929
930
|
function asRecord(value) {
|
|
930
931
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
@@ -1122,12 +1123,10 @@ var ClaudeCodeHost = class {
|
|
|
1122
1123
|
/** Alfe sessionId → claude session id map (mirrors ~/.alfe/claude-code/sessions.json). */
|
|
1123
1124
|
claudeSessionIds = /* @__PURE__ */ new Map();
|
|
1124
1125
|
claudeSessionsPath;
|
|
1125
|
-
/**
|
|
1126
|
+
/** Session → one running turn's wire identity and owned child. */
|
|
1126
1127
|
runningTurns = /* @__PURE__ */ new Map();
|
|
1127
|
-
/**
|
|
1128
|
-
|
|
1129
|
-
/** RPC sessionKey → resolved SessionDriver so chat.abort can SIGINT the child. */
|
|
1130
|
-
activeDriverBySession = /* @__PURE__ */ new Map();
|
|
1128
|
+
/** Process-local admission/result replay. In-flight records are never evicted. */
|
|
1129
|
+
agentOperations = /* @__PURE__ */ new Map();
|
|
1131
1130
|
claudeSessionSave = Promise.resolve();
|
|
1132
1131
|
stopping = false;
|
|
1133
1132
|
stopPromise = null;
|
|
@@ -1166,7 +1165,10 @@ var ClaudeCodeHost = class {
|
|
|
1166
1165
|
if (this.stopPromise) return this.stopPromise;
|
|
1167
1166
|
this.stopping = true;
|
|
1168
1167
|
this.queues.stop();
|
|
1169
|
-
for (const driver of this.
|
|
1168
|
+
for (const { driver, cancellation } of this.runningTurns.values()) {
|
|
1169
|
+
cancellation.abort();
|
|
1170
|
+
driver.abort();
|
|
1171
|
+
}
|
|
1170
1172
|
this.chat.stop();
|
|
1171
1173
|
this.stopPromise = (async () => {
|
|
1172
1174
|
let timer;
|
|
@@ -1204,7 +1206,7 @@ var ClaudeCodeHost = class {
|
|
|
1204
1206
|
await this.handleSessionsGet(req);
|
|
1205
1207
|
return;
|
|
1206
1208
|
case "runtime.activity":
|
|
1207
|
-
this.chat.sendResponse(req.id, true, { active: this.
|
|
1209
|
+
this.chat.sendResponse(req.id, true, { active: this.runningTurns.size > 0 });
|
|
1208
1210
|
return;
|
|
1209
1211
|
default:
|
|
1210
1212
|
this.chat.sendResponse(req.id, false, { message: `Unknown method: ${req.method}` });
|
|
@@ -1221,29 +1223,63 @@ var ClaudeCodeHost = class {
|
|
|
1221
1223
|
const sessionKey = params.sessionKey;
|
|
1222
1224
|
const idem = params.idempotencyKey;
|
|
1223
1225
|
if (idem) this.pruneIdempotency();
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
+
const fingerprint = (0, node_crypto.createHash)("sha256").update(JSON.stringify(params)).digest("hex");
|
|
1227
|
+
const existing = idem ? this.agentOperations.get(idem) : void 0;
|
|
1228
|
+
if (existing) {
|
|
1229
|
+
if (existing.fingerprint !== fingerprint) throw new Error("Idempotency key already belongs to a different agent request");
|
|
1230
|
+
if (existing.outcome) this.deliverAgentOutcome(req.id, existing.outcome);
|
|
1231
|
+
else {
|
|
1232
|
+
const outcome = await existing.completion;
|
|
1233
|
+
this.chat.sendResponse(req.id, outcome.ok, outcome.payload);
|
|
1234
|
+
}
|
|
1226
1235
|
return;
|
|
1227
1236
|
}
|
|
1228
|
-
|
|
1229
|
-
|
|
1237
|
+
let resolveCompletion;
|
|
1238
|
+
const operation = {
|
|
1239
|
+
fingerprint,
|
|
1240
|
+
completion: new Promise((resolve) => {
|
|
1241
|
+
resolveCompletion = resolve;
|
|
1242
|
+
}),
|
|
1243
|
+
resultBytes: 0
|
|
1244
|
+
};
|
|
1245
|
+
if (idem) this.agentOperations.set(idem, operation);
|
|
1246
|
+
const complete = (outcome) => {
|
|
1247
|
+
if (operation.outcome) return;
|
|
1248
|
+
operation.outcome = outcome;
|
|
1249
|
+
operation.settledAt = Date.now();
|
|
1250
|
+
operation.resultBytes = Buffer.byteLength(JSON.stringify(outcome), "utf-8");
|
|
1251
|
+
this.deliverAgentOutcome(req.id, outcome);
|
|
1252
|
+
resolveCompletion(outcome);
|
|
1253
|
+
if (idem) this.pruneIdempotency();
|
|
1254
|
+
};
|
|
1230
1255
|
const coalesceKey = params.coalesce === "replace" ? sessionKey : void 0;
|
|
1231
1256
|
try {
|
|
1232
|
-
await this.queues.enqueue(sessionKey, () => this.runAgentTurn(req, params), coalesceKey ? {
|
|
1257
|
+
await this.queues.enqueue(sessionKey, () => this.runAgentTurn(req, params, complete), coalesceKey ? {
|
|
1233
1258
|
coalesceKey,
|
|
1234
1259
|
onSuperseded: () => {
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1260
|
+
complete({
|
|
1261
|
+
ok: true,
|
|
1262
|
+
payload: {
|
|
1263
|
+
superseded: true,
|
|
1264
|
+
sessionKey
|
|
1265
|
+
}
|
|
1238
1266
|
});
|
|
1239
1267
|
}
|
|
1240
1268
|
} : void 0);
|
|
1241
1269
|
} catch (error) {
|
|
1242
|
-
|
|
1243
|
-
|
|
1270
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1271
|
+
this.log.error(`[claude-host] agent operation failed: ${message}`);
|
|
1272
|
+
complete({
|
|
1273
|
+
ok: false,
|
|
1274
|
+
payload: { message }
|
|
1275
|
+
});
|
|
1244
1276
|
}
|
|
1245
1277
|
}
|
|
1246
|
-
|
|
1278
|
+
deliverAgentOutcome(requestId, outcome) {
|
|
1279
|
+
if (outcome.notification) this.chat.notify("agent-message", outcome.notification);
|
|
1280
|
+
this.chat.sendResponse(requestId, outcome.ok, outcome.payload);
|
|
1281
|
+
}
|
|
1282
|
+
async runAgentTurn(req, params, complete) {
|
|
1247
1283
|
const sessionKey = params.sessionKey;
|
|
1248
1284
|
const bareSessionId = params.conversationId ?? stripAgentPrefix(sessionKey);
|
|
1249
1285
|
const runId = params.idempotencyKey ?? req.id;
|
|
@@ -1260,11 +1296,15 @@ var ClaudeCodeHost = class {
|
|
|
1260
1296
|
const appendSystemPrompt = this.buildSystemPrompt(memoryContext, isVoice);
|
|
1261
1297
|
const claudeSessionId = this.claudeSessionIds.get(bareSessionId) ?? null;
|
|
1262
1298
|
const driver = this.makeDriver();
|
|
1263
|
-
|
|
1264
|
-
this.
|
|
1299
|
+
const cancellation = new AbortController();
|
|
1300
|
+
this.runningTurns.set(sessionKey, {
|
|
1301
|
+
runId,
|
|
1302
|
+
driver,
|
|
1303
|
+
cancellation
|
|
1304
|
+
});
|
|
1265
1305
|
let result;
|
|
1266
1306
|
try {
|
|
1267
|
-
result = await this.runWithResumeFallback(driver, bareSessionId,
|
|
1307
|
+
result = await this.runWithResumeFallback(driver, bareSessionId, cancellation.signal, {
|
|
1268
1308
|
prompt: params.message,
|
|
1269
1309
|
claudeSessionId,
|
|
1270
1310
|
cwd: this.cfg.workspace,
|
|
@@ -1277,8 +1317,6 @@ var ClaudeCodeHost = class {
|
|
|
1277
1317
|
}, isVoice);
|
|
1278
1318
|
} finally {
|
|
1279
1319
|
this.runningTurns.delete(sessionKey);
|
|
1280
|
-
this.activeDriverBySession.delete(sessionKey);
|
|
1281
|
-
if (params.idempotencyKey) this.seenIdempotencyKeys.set(params.idempotencyKey, Date.now());
|
|
1282
1320
|
}
|
|
1283
1321
|
if (result.claudeSessionId && result.claudeSessionId !== claudeSessionId) {
|
|
1284
1322
|
this.rememberClaudeSessionId(bareSessionId, result.claudeSessionId);
|
|
@@ -1286,19 +1324,32 @@ var ClaudeCodeHost = class {
|
|
|
1286
1324
|
}
|
|
1287
1325
|
const activityRecords = collectTurnActivityRecords(result.emissions);
|
|
1288
1326
|
if (activityRecords.length) await appendActivity(bareSessionId, activityRecords);
|
|
1289
|
-
if (result.aborted)
|
|
1327
|
+
if (result.aborted) {
|
|
1328
|
+
complete({
|
|
1329
|
+
ok: true,
|
|
1330
|
+
payload: { sessionKey }
|
|
1331
|
+
});
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1290
1334
|
if (result.isError) {
|
|
1291
|
-
|
|
1335
|
+
complete({
|
|
1336
|
+
ok: false,
|
|
1337
|
+
payload: { message: result.errorMessage ?? "Claude Code turn failed" }
|
|
1338
|
+
});
|
|
1292
1339
|
return;
|
|
1293
1340
|
}
|
|
1294
1341
|
if (result.finalText) await addMessage(bareSessionId, "assistant", result.finalText);
|
|
1295
|
-
|
|
1342
|
+
const notification = result.finalText ? {
|
|
1296
1343
|
conversationId: bareSessionId,
|
|
1297
1344
|
text: result.finalText,
|
|
1298
1345
|
sessionKey,
|
|
1299
1346
|
messageId: params.chatMessageId ?? req.id
|
|
1347
|
+
} : void 0;
|
|
1348
|
+
complete({
|
|
1349
|
+
ok: true,
|
|
1350
|
+
payload: { sessionKey },
|
|
1351
|
+
...notification ? { notification } : {}
|
|
1300
1352
|
});
|
|
1301
|
-
this.chat.sendResponse(req.id, true, { sessionKey });
|
|
1302
1353
|
try {
|
|
1303
1354
|
const messages = [{
|
|
1304
1355
|
role: "user",
|
|
@@ -1321,18 +1372,30 @@ var ClaudeCodeHost = class {
|
|
|
1321
1372
|
* Run a turn; on a `--resume` failure (stale/missing claude session id) start
|
|
1322
1373
|
* fresh and prepend the last-N stored messages so the model keeps context.
|
|
1323
1374
|
*/
|
|
1324
|
-
async runWithResumeFallback(driver, bareSessionId,
|
|
1375
|
+
async runWithResumeFallback(driver, bareSessionId, cancellation, baseParams, isVoice) {
|
|
1376
|
+
const cancelled = () => cancellation.aborted || this.stopping;
|
|
1377
|
+
const aborted = (result) => ({
|
|
1378
|
+
claudeSessionId: result?.claudeSessionId ?? null,
|
|
1379
|
+
emissions: result?.emissions ?? [],
|
|
1380
|
+
isError: false,
|
|
1381
|
+
finalText: "",
|
|
1382
|
+
aborted: true
|
|
1383
|
+
});
|
|
1384
|
+
if (cancelled()) return aborted();
|
|
1325
1385
|
const first = await driver.runTurn(baseParams, isVoice ? "voice" : "chat");
|
|
1326
|
-
if (
|
|
1386
|
+
if (cancelled()) return aborted(first);
|
|
1387
|
+
if (!(baseParams.claudeSessionId !== null && first.isError && first.claudeSessionId === null && first.emissions.length === 0) || first.aborted) return first;
|
|
1327
1388
|
this.log.warn(`[claude-host] --resume failed for ${bareSessionId}; starting a fresh claude session`);
|
|
1328
1389
|
this.claudeSessionIds.delete(bareSessionId);
|
|
1329
1390
|
const history = await this.buildHistoryPreamble(bareSessionId);
|
|
1391
|
+
if (cancelled()) return aborted(first);
|
|
1330
1392
|
const freshPrompt = history ? `${history}\n\n---\n\n${baseParams.prompt}` : baseParams.prompt;
|
|
1331
|
-
|
|
1393
|
+
const fresh = await driver.runTurn({
|
|
1332
1394
|
...baseParams,
|
|
1333
1395
|
claudeSessionId: null,
|
|
1334
1396
|
prompt: freshPrompt
|
|
1335
1397
|
}, isVoice ? "voice" : "chat");
|
|
1398
|
+
return cancelled() ? aborted(fresh) : fresh;
|
|
1336
1399
|
}
|
|
1337
1400
|
forwardEmission(runId, sessionKey, emission) {
|
|
1338
1401
|
if (emission.type === "chat-delta") {
|
|
@@ -1368,13 +1431,13 @@ var ClaudeCodeHost = class {
|
|
|
1368
1431
|
}
|
|
1369
1432
|
}
|
|
1370
1433
|
handleChatAbort(req) {
|
|
1371
|
-
const { sessionKey } = req.params;
|
|
1434
|
+
const { sessionKey, runId } = req.params;
|
|
1372
1435
|
let aborted = false;
|
|
1373
|
-
const
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1436
|
+
const active = sessionKey ? this.runningTurns.get(sessionKey) : void 0;
|
|
1437
|
+
const matchesRun = runId === void 0 || typeof runId === "string" && runId.length > 0 && runId.length <= 256 && active?.runId === runId;
|
|
1438
|
+
if (sessionKey && active && matchesRun) {
|
|
1439
|
+
active.cancellation.abort();
|
|
1440
|
+
active.driver.abort();
|
|
1378
1441
|
aborted = true;
|
|
1379
1442
|
}
|
|
1380
1443
|
this.chat.sendResponse(req.id, true, { aborted });
|
|
@@ -1477,11 +1540,14 @@ var ClaudeCodeHost = class {
|
|
|
1477
1540
|
}
|
|
1478
1541
|
pruneIdempotency() {
|
|
1479
1542
|
const cutoff = Date.now() - 10 * 6e4;
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
this.
|
|
1543
|
+
let resultBytes = 0;
|
|
1544
|
+
for (const [key, operation] of this.agentOperations) if (operation.settledAt !== void 0 && operation.settledAt < cutoff) this.agentOperations.delete(key);
|
|
1545
|
+
else resultBytes += operation.resultBytes;
|
|
1546
|
+
for (const [key, operation] of this.agentOperations) {
|
|
1547
|
+
if (this.agentOperations.size < MAX_IDEMPOTENCY_KEYS && resultBytes <= MAX_IDEMPOTENCY_RESULT_BYTES) break;
|
|
1548
|
+
if (operation.settledAt === void 0) continue;
|
|
1549
|
+
this.agentOperations.delete(key);
|
|
1550
|
+
resultBytes -= operation.resultBytes;
|
|
1485
1551
|
}
|
|
1486
1552
|
}
|
|
1487
1553
|
};
|
package/dist/host-config.js
CHANGED
|
@@ -3,9 +3,9 @@ import { ChatServiceClient, resolveAlfeChat } from "@alfe.ai/chat";
|
|
|
3
3
|
import { mkdir, readFile, readdir, rename, stat, unlink, writeFile } from "node:fs/promises";
|
|
4
4
|
import { basename, dirname, join } from "node:path";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
6
7
|
import { spawn } from "node:child_process";
|
|
7
8
|
import { createInterface } from "node:readline";
|
|
8
|
-
import { randomUUID } from "node:crypto";
|
|
9
9
|
import { existsSync } from "node:fs";
|
|
10
10
|
import { resolveConfig } from "@alfe.ai/config";
|
|
11
11
|
//#region src/child-env.ts
|
|
@@ -925,6 +925,7 @@ const MAX_IDENTIFIER_CHARS = 512;
|
|
|
925
925
|
const MAX_MEMORY_CONTEXT_CHARS = 64 * 1024;
|
|
926
926
|
const MAX_CLAUDE_SESSION_IDS = 1e3;
|
|
927
927
|
const MAX_IDEMPOTENCY_KEYS = 1e4;
|
|
928
|
+
const MAX_IDEMPOTENCY_RESULT_BYTES = 8 * 1024 * 1024;
|
|
928
929
|
const SHUTDOWN_GRACE_MS = 3e3;
|
|
929
930
|
function asRecord(value) {
|
|
930
931
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
@@ -1122,12 +1123,10 @@ var ClaudeCodeHost = class {
|
|
|
1122
1123
|
/** Alfe sessionId → claude session id map (mirrors ~/.alfe/claude-code/sessions.json). */
|
|
1123
1124
|
claudeSessionIds = /* @__PURE__ */ new Map();
|
|
1124
1125
|
claudeSessionsPath;
|
|
1125
|
-
/**
|
|
1126
|
+
/** Session → one running turn's wire identity and owned child. */
|
|
1126
1127
|
runningTurns = /* @__PURE__ */ new Map();
|
|
1127
|
-
/**
|
|
1128
|
-
|
|
1129
|
-
/** RPC sessionKey → resolved SessionDriver so chat.abort can SIGINT the child. */
|
|
1130
|
-
activeDriverBySession = /* @__PURE__ */ new Map();
|
|
1128
|
+
/** Process-local admission/result replay. In-flight records are never evicted. */
|
|
1129
|
+
agentOperations = /* @__PURE__ */ new Map();
|
|
1131
1130
|
claudeSessionSave = Promise.resolve();
|
|
1132
1131
|
stopping = false;
|
|
1133
1132
|
stopPromise = null;
|
|
@@ -1166,7 +1165,10 @@ var ClaudeCodeHost = class {
|
|
|
1166
1165
|
if (this.stopPromise) return this.stopPromise;
|
|
1167
1166
|
this.stopping = true;
|
|
1168
1167
|
this.queues.stop();
|
|
1169
|
-
for (const driver of this.
|
|
1168
|
+
for (const { driver, cancellation } of this.runningTurns.values()) {
|
|
1169
|
+
cancellation.abort();
|
|
1170
|
+
driver.abort();
|
|
1171
|
+
}
|
|
1170
1172
|
this.chat.stop();
|
|
1171
1173
|
this.stopPromise = (async () => {
|
|
1172
1174
|
let timer;
|
|
@@ -1204,7 +1206,7 @@ var ClaudeCodeHost = class {
|
|
|
1204
1206
|
await this.handleSessionsGet(req);
|
|
1205
1207
|
return;
|
|
1206
1208
|
case "runtime.activity":
|
|
1207
|
-
this.chat.sendResponse(req.id, true, { active: this.
|
|
1209
|
+
this.chat.sendResponse(req.id, true, { active: this.runningTurns.size > 0 });
|
|
1208
1210
|
return;
|
|
1209
1211
|
default:
|
|
1210
1212
|
this.chat.sendResponse(req.id, false, { message: `Unknown method: ${req.method}` });
|
|
@@ -1221,29 +1223,63 @@ var ClaudeCodeHost = class {
|
|
|
1221
1223
|
const sessionKey = params.sessionKey;
|
|
1222
1224
|
const idem = params.idempotencyKey;
|
|
1223
1225
|
if (idem) this.pruneIdempotency();
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
+
const fingerprint = createHash("sha256").update(JSON.stringify(params)).digest("hex");
|
|
1227
|
+
const existing = idem ? this.agentOperations.get(idem) : void 0;
|
|
1228
|
+
if (existing) {
|
|
1229
|
+
if (existing.fingerprint !== fingerprint) throw new Error("Idempotency key already belongs to a different agent request");
|
|
1230
|
+
if (existing.outcome) this.deliverAgentOutcome(req.id, existing.outcome);
|
|
1231
|
+
else {
|
|
1232
|
+
const outcome = await existing.completion;
|
|
1233
|
+
this.chat.sendResponse(req.id, outcome.ok, outcome.payload);
|
|
1234
|
+
}
|
|
1226
1235
|
return;
|
|
1227
1236
|
}
|
|
1228
|
-
|
|
1229
|
-
|
|
1237
|
+
let resolveCompletion;
|
|
1238
|
+
const operation = {
|
|
1239
|
+
fingerprint,
|
|
1240
|
+
completion: new Promise((resolve) => {
|
|
1241
|
+
resolveCompletion = resolve;
|
|
1242
|
+
}),
|
|
1243
|
+
resultBytes: 0
|
|
1244
|
+
};
|
|
1245
|
+
if (idem) this.agentOperations.set(idem, operation);
|
|
1246
|
+
const complete = (outcome) => {
|
|
1247
|
+
if (operation.outcome) return;
|
|
1248
|
+
operation.outcome = outcome;
|
|
1249
|
+
operation.settledAt = Date.now();
|
|
1250
|
+
operation.resultBytes = Buffer.byteLength(JSON.stringify(outcome), "utf-8");
|
|
1251
|
+
this.deliverAgentOutcome(req.id, outcome);
|
|
1252
|
+
resolveCompletion(outcome);
|
|
1253
|
+
if (idem) this.pruneIdempotency();
|
|
1254
|
+
};
|
|
1230
1255
|
const coalesceKey = params.coalesce === "replace" ? sessionKey : void 0;
|
|
1231
1256
|
try {
|
|
1232
|
-
await this.queues.enqueue(sessionKey, () => this.runAgentTurn(req, params), coalesceKey ? {
|
|
1257
|
+
await this.queues.enqueue(sessionKey, () => this.runAgentTurn(req, params, complete), coalesceKey ? {
|
|
1233
1258
|
coalesceKey,
|
|
1234
1259
|
onSuperseded: () => {
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1260
|
+
complete({
|
|
1261
|
+
ok: true,
|
|
1262
|
+
payload: {
|
|
1263
|
+
superseded: true,
|
|
1264
|
+
sessionKey
|
|
1265
|
+
}
|
|
1238
1266
|
});
|
|
1239
1267
|
}
|
|
1240
1268
|
} : void 0);
|
|
1241
1269
|
} catch (error) {
|
|
1242
|
-
|
|
1243
|
-
|
|
1270
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1271
|
+
this.log.error(`[claude-host] agent operation failed: ${message}`);
|
|
1272
|
+
complete({
|
|
1273
|
+
ok: false,
|
|
1274
|
+
payload: { message }
|
|
1275
|
+
});
|
|
1244
1276
|
}
|
|
1245
1277
|
}
|
|
1246
|
-
|
|
1278
|
+
deliverAgentOutcome(requestId, outcome) {
|
|
1279
|
+
if (outcome.notification) this.chat.notify("agent-message", outcome.notification);
|
|
1280
|
+
this.chat.sendResponse(requestId, outcome.ok, outcome.payload);
|
|
1281
|
+
}
|
|
1282
|
+
async runAgentTurn(req, params, complete) {
|
|
1247
1283
|
const sessionKey = params.sessionKey;
|
|
1248
1284
|
const bareSessionId = params.conversationId ?? stripAgentPrefix(sessionKey);
|
|
1249
1285
|
const runId = params.idempotencyKey ?? req.id;
|
|
@@ -1260,11 +1296,15 @@ var ClaudeCodeHost = class {
|
|
|
1260
1296
|
const appendSystemPrompt = this.buildSystemPrompt(memoryContext, isVoice);
|
|
1261
1297
|
const claudeSessionId = this.claudeSessionIds.get(bareSessionId) ?? null;
|
|
1262
1298
|
const driver = this.makeDriver();
|
|
1263
|
-
|
|
1264
|
-
this.
|
|
1299
|
+
const cancellation = new AbortController();
|
|
1300
|
+
this.runningTurns.set(sessionKey, {
|
|
1301
|
+
runId,
|
|
1302
|
+
driver,
|
|
1303
|
+
cancellation
|
|
1304
|
+
});
|
|
1265
1305
|
let result;
|
|
1266
1306
|
try {
|
|
1267
|
-
result = await this.runWithResumeFallback(driver, bareSessionId,
|
|
1307
|
+
result = await this.runWithResumeFallback(driver, bareSessionId, cancellation.signal, {
|
|
1268
1308
|
prompt: params.message,
|
|
1269
1309
|
claudeSessionId,
|
|
1270
1310
|
cwd: this.cfg.workspace,
|
|
@@ -1277,8 +1317,6 @@ var ClaudeCodeHost = class {
|
|
|
1277
1317
|
}, isVoice);
|
|
1278
1318
|
} finally {
|
|
1279
1319
|
this.runningTurns.delete(sessionKey);
|
|
1280
|
-
this.activeDriverBySession.delete(sessionKey);
|
|
1281
|
-
if (params.idempotencyKey) this.seenIdempotencyKeys.set(params.idempotencyKey, Date.now());
|
|
1282
1320
|
}
|
|
1283
1321
|
if (result.claudeSessionId && result.claudeSessionId !== claudeSessionId) {
|
|
1284
1322
|
this.rememberClaudeSessionId(bareSessionId, result.claudeSessionId);
|
|
@@ -1286,19 +1324,32 @@ var ClaudeCodeHost = class {
|
|
|
1286
1324
|
}
|
|
1287
1325
|
const activityRecords = collectTurnActivityRecords(result.emissions);
|
|
1288
1326
|
if (activityRecords.length) await appendActivity(bareSessionId, activityRecords);
|
|
1289
|
-
if (result.aborted)
|
|
1327
|
+
if (result.aborted) {
|
|
1328
|
+
complete({
|
|
1329
|
+
ok: true,
|
|
1330
|
+
payload: { sessionKey }
|
|
1331
|
+
});
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1290
1334
|
if (result.isError) {
|
|
1291
|
-
|
|
1335
|
+
complete({
|
|
1336
|
+
ok: false,
|
|
1337
|
+
payload: { message: result.errorMessage ?? "Claude Code turn failed" }
|
|
1338
|
+
});
|
|
1292
1339
|
return;
|
|
1293
1340
|
}
|
|
1294
1341
|
if (result.finalText) await addMessage(bareSessionId, "assistant", result.finalText);
|
|
1295
|
-
|
|
1342
|
+
const notification = result.finalText ? {
|
|
1296
1343
|
conversationId: bareSessionId,
|
|
1297
1344
|
text: result.finalText,
|
|
1298
1345
|
sessionKey,
|
|
1299
1346
|
messageId: params.chatMessageId ?? req.id
|
|
1347
|
+
} : void 0;
|
|
1348
|
+
complete({
|
|
1349
|
+
ok: true,
|
|
1350
|
+
payload: { sessionKey },
|
|
1351
|
+
...notification ? { notification } : {}
|
|
1300
1352
|
});
|
|
1301
|
-
this.chat.sendResponse(req.id, true, { sessionKey });
|
|
1302
1353
|
try {
|
|
1303
1354
|
const messages = [{
|
|
1304
1355
|
role: "user",
|
|
@@ -1321,18 +1372,30 @@ var ClaudeCodeHost = class {
|
|
|
1321
1372
|
* Run a turn; on a `--resume` failure (stale/missing claude session id) start
|
|
1322
1373
|
* fresh and prepend the last-N stored messages so the model keeps context.
|
|
1323
1374
|
*/
|
|
1324
|
-
async runWithResumeFallback(driver, bareSessionId,
|
|
1375
|
+
async runWithResumeFallback(driver, bareSessionId, cancellation, baseParams, isVoice) {
|
|
1376
|
+
const cancelled = () => cancellation.aborted || this.stopping;
|
|
1377
|
+
const aborted = (result) => ({
|
|
1378
|
+
claudeSessionId: result?.claudeSessionId ?? null,
|
|
1379
|
+
emissions: result?.emissions ?? [],
|
|
1380
|
+
isError: false,
|
|
1381
|
+
finalText: "",
|
|
1382
|
+
aborted: true
|
|
1383
|
+
});
|
|
1384
|
+
if (cancelled()) return aborted();
|
|
1325
1385
|
const first = await driver.runTurn(baseParams, isVoice ? "voice" : "chat");
|
|
1326
|
-
if (
|
|
1386
|
+
if (cancelled()) return aborted(first);
|
|
1387
|
+
if (!(baseParams.claudeSessionId !== null && first.isError && first.claudeSessionId === null && first.emissions.length === 0) || first.aborted) return first;
|
|
1327
1388
|
this.log.warn(`[claude-host] --resume failed for ${bareSessionId}; starting a fresh claude session`);
|
|
1328
1389
|
this.claudeSessionIds.delete(bareSessionId);
|
|
1329
1390
|
const history = await this.buildHistoryPreamble(bareSessionId);
|
|
1391
|
+
if (cancelled()) return aborted(first);
|
|
1330
1392
|
const freshPrompt = history ? `${history}\n\n---\n\n${baseParams.prompt}` : baseParams.prompt;
|
|
1331
|
-
|
|
1393
|
+
const fresh = await driver.runTurn({
|
|
1332
1394
|
...baseParams,
|
|
1333
1395
|
claudeSessionId: null,
|
|
1334
1396
|
prompt: freshPrompt
|
|
1335
1397
|
}, isVoice ? "voice" : "chat");
|
|
1398
|
+
return cancelled() ? aborted(fresh) : fresh;
|
|
1336
1399
|
}
|
|
1337
1400
|
forwardEmission(runId, sessionKey, emission) {
|
|
1338
1401
|
if (emission.type === "chat-delta") {
|
|
@@ -1368,13 +1431,13 @@ var ClaudeCodeHost = class {
|
|
|
1368
1431
|
}
|
|
1369
1432
|
}
|
|
1370
1433
|
handleChatAbort(req) {
|
|
1371
|
-
const { sessionKey } = req.params;
|
|
1434
|
+
const { sessionKey, runId } = req.params;
|
|
1372
1435
|
let aborted = false;
|
|
1373
|
-
const
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1436
|
+
const active = sessionKey ? this.runningTurns.get(sessionKey) : void 0;
|
|
1437
|
+
const matchesRun = runId === void 0 || typeof runId === "string" && runId.length > 0 && runId.length <= 256 && active?.runId === runId;
|
|
1438
|
+
if (sessionKey && active && matchesRun) {
|
|
1439
|
+
active.cancellation.abort();
|
|
1440
|
+
active.driver.abort();
|
|
1378
1441
|
aborted = true;
|
|
1379
1442
|
}
|
|
1380
1443
|
this.chat.sendResponse(req.id, true, { aborted });
|
|
@@ -1477,11 +1540,14 @@ var ClaudeCodeHost = class {
|
|
|
1477
1540
|
}
|
|
1478
1541
|
pruneIdempotency() {
|
|
1479
1542
|
const cutoff = Date.now() - 10 * 6e4;
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
this.
|
|
1543
|
+
let resultBytes = 0;
|
|
1544
|
+
for (const [key, operation] of this.agentOperations) if (operation.settledAt !== void 0 && operation.settledAt < cutoff) this.agentOperations.delete(key);
|
|
1545
|
+
else resultBytes += operation.resultBytes;
|
|
1546
|
+
for (const [key, operation] of this.agentOperations) {
|
|
1547
|
+
if (this.agentOperations.size < MAX_IDEMPOTENCY_KEYS && resultBytes <= MAX_IDEMPOTENCY_RESULT_BYTES) break;
|
|
1548
|
+
if (operation.settledAt === void 0) continue;
|
|
1549
|
+
this.agentOperations.delete(key);
|
|
1550
|
+
resultBytes -= operation.resultBytes;
|
|
1485
1551
|
}
|
|
1486
1552
|
}
|
|
1487
1553
|
};
|
package/dist/index.d.cts
CHANGED
|
@@ -2396,12 +2396,10 @@ declare class ClaudeCodeHost {
|
|
|
2396
2396
|
/** Alfe sessionId → claude session id map (mirrors ~/.alfe/claude-code/sessions.json). */
|
|
2397
2397
|
private readonly claudeSessionIds;
|
|
2398
2398
|
private claudeSessionsPath;
|
|
2399
|
-
/**
|
|
2399
|
+
/** Session → one running turn's wire identity and owned child. */
|
|
2400
2400
|
private readonly runningTurns;
|
|
2401
|
-
/**
|
|
2402
|
-
private readonly
|
|
2403
|
-
/** RPC sessionKey → resolved SessionDriver so chat.abort can SIGINT the child. */
|
|
2404
|
-
private activeDriverBySession;
|
|
2401
|
+
/** Process-local admission/result replay. In-flight records are never evicted. */
|
|
2402
|
+
private readonly agentOperations;
|
|
2405
2403
|
private claudeSessionSave;
|
|
2406
2404
|
private stopping;
|
|
2407
2405
|
private stopPromise;
|
|
@@ -2421,6 +2419,7 @@ declare class ClaudeCodeHost {
|
|
|
2421
2419
|
*/
|
|
2422
2420
|
handleRequest(req: RpcRequest): Promise<void>;
|
|
2423
2421
|
private handleAgent;
|
|
2422
|
+
private deliverAgentOutcome;
|
|
2424
2423
|
private runAgentTurn;
|
|
2425
2424
|
/**
|
|
2426
2425
|
* Run a turn; on a `--resume` failure (stale/missing claude session id) start
|
package/dist/index.d.ts
CHANGED
|
@@ -2396,12 +2396,10 @@ declare class ClaudeCodeHost {
|
|
|
2396
2396
|
/** Alfe sessionId → claude session id map (mirrors ~/.alfe/claude-code/sessions.json). */
|
|
2397
2397
|
private readonly claudeSessionIds;
|
|
2398
2398
|
private claudeSessionsPath;
|
|
2399
|
-
/**
|
|
2399
|
+
/** Session → one running turn's wire identity and owned child. */
|
|
2400
2400
|
private readonly runningTurns;
|
|
2401
|
-
/**
|
|
2402
|
-
private readonly
|
|
2403
|
-
/** RPC sessionKey → resolved SessionDriver so chat.abort can SIGINT the child. */
|
|
2404
|
-
private activeDriverBySession;
|
|
2401
|
+
/** Process-local admission/result replay. In-flight records are never evicted. */
|
|
2402
|
+
private readonly agentOperations;
|
|
2405
2403
|
private claudeSessionSave;
|
|
2406
2404
|
private stopping;
|
|
2407
2405
|
private stopPromise;
|
|
@@ -2421,6 +2419,7 @@ declare class ClaudeCodeHost {
|
|
|
2421
2419
|
*/
|
|
2422
2420
|
handleRequest(req: RpcRequest): Promise<void>;
|
|
2423
2421
|
private handleAgent;
|
|
2422
|
+
private deliverAgentOutcome;
|
|
2424
2423
|
private runAgentTurn;
|
|
2425
2424
|
/**
|
|
2426
2425
|
* Run a turn; on a `--resume` failure (stale/missing claude session id) start
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alfe.ai/claude-code-host",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"description": "Chat bridge that makes a self-hosted Claude Code agent reachable from Alfe chat on the user's Claude subscription",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|