@alfe.ai/claude-code-host 0.1.12 → 0.1.14

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.
@@ -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
- /** RPC sessionKey → currently RUNNING request id (for chat.abort targeting). */
1126
+ /** Session → one running turn's wire identity and owned child. */
1126
1127
  runningTurns = /* @__PURE__ */ new Map();
1127
- /** idempotencyKey → settled request id, for E3 duplicate-RPC dedupe. */
1128
- seenIdempotencyKeys = /* @__PURE__ */ new Map();
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.activeDriverBySession.values()) driver.abort();
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.activeDriverBySession.size > 0 });
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
- if (idem && this.seenIdempotencyKeys.has(idem)) {
1225
- this.chat.sendResponse(req.id, true, { sessionKey });
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
- const claimedAt = idem ? Date.now() : void 0;
1229
- if (idem && claimedAt !== void 0) this.seenIdempotencyKeys.set(idem, claimedAt);
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
- this.chat.sendResponse(req.id, true, {
1236
- superseded: true,
1237
- sessionKey
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
- if (idem && this.seenIdempotencyKeys.get(idem) === claimedAt) this.seenIdempotencyKeys.delete(idem);
1243
- throw error;
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
- async runAgentTurn(req, params) {
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
- this.runningTurns.set(sessionKey, req.id);
1264
- this.activeDriverBySession.set(sessionKey, driver);
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, sessionKey, runId, {
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) return;
1327
+ if (result.aborted) {
1328
+ complete({
1329
+ ok: true,
1330
+ payload: { sessionKey }
1331
+ });
1332
+ return;
1333
+ }
1290
1334
  if (result.isError) {
1291
- this.chat.sendResponse(req.id, false, { message: result.errorMessage ?? "Claude Code turn failed" });
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
- if (result.finalText) this.chat.notify("agent-message", {
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, _sessionKey, _runId, baseParams, isVoice) {
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 (!(baseParams.claudeSessionId !== null && first.isError && first.claudeSessionId === null && first.emissions.length === 0) || this.stopping) return first;
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
- return driver.runTurn({
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 driver = sessionKey ? this.activeDriverBySession.get(sessionKey) : void 0;
1374
- if (sessionKey && driver) {
1375
- driver.abort();
1376
- const runningReqId = this.runningTurns.get(sessionKey);
1377
- if (runningReqId) this.chat.sendResponse(runningReqId, true, { sessionKey });
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
- for (const [k, ts] of this.seenIdempotencyKeys) if (ts < cutoff) this.seenIdempotencyKeys.delete(k);
1481
- while (this.seenIdempotencyKeys.size >= MAX_IDEMPOTENCY_KEYS) {
1482
- const oldest = this.seenIdempotencyKeys.keys().next().value;
1483
- if (!oldest) break;
1484
- this.seenIdempotencyKeys.delete(oldest);
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
  };
@@ -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
- /** RPC sessionKey → currently RUNNING request id (for chat.abort targeting). */
1126
+ /** Session → one running turn's wire identity and owned child. */
1126
1127
  runningTurns = /* @__PURE__ */ new Map();
1127
- /** idempotencyKey → settled request id, for E3 duplicate-RPC dedupe. */
1128
- seenIdempotencyKeys = /* @__PURE__ */ new Map();
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.activeDriverBySession.values()) driver.abort();
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.activeDriverBySession.size > 0 });
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
- if (idem && this.seenIdempotencyKeys.has(idem)) {
1225
- this.chat.sendResponse(req.id, true, { sessionKey });
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
- const claimedAt = idem ? Date.now() : void 0;
1229
- if (idem && claimedAt !== void 0) this.seenIdempotencyKeys.set(idem, claimedAt);
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
- this.chat.sendResponse(req.id, true, {
1236
- superseded: true,
1237
- sessionKey
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
- if (idem && this.seenIdempotencyKeys.get(idem) === claimedAt) this.seenIdempotencyKeys.delete(idem);
1243
- throw error;
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
- async runAgentTurn(req, params) {
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
- this.runningTurns.set(sessionKey, req.id);
1264
- this.activeDriverBySession.set(sessionKey, driver);
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, sessionKey, runId, {
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) return;
1327
+ if (result.aborted) {
1328
+ complete({
1329
+ ok: true,
1330
+ payload: { sessionKey }
1331
+ });
1332
+ return;
1333
+ }
1290
1334
  if (result.isError) {
1291
- this.chat.sendResponse(req.id, false, { message: result.errorMessage ?? "Claude Code turn failed" });
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
- if (result.finalText) this.chat.notify("agent-message", {
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, _sessionKey, _runId, baseParams, isVoice) {
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 (!(baseParams.claudeSessionId !== null && first.isError && first.claudeSessionId === null && first.emissions.length === 0) || this.stopping) return first;
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
- return driver.runTurn({
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 driver = sessionKey ? this.activeDriverBySession.get(sessionKey) : void 0;
1374
- if (sessionKey && driver) {
1375
- driver.abort();
1376
- const runningReqId = this.runningTurns.get(sessionKey);
1377
- if (runningReqId) this.chat.sendResponse(runningReqId, true, { sessionKey });
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
- for (const [k, ts] of this.seenIdempotencyKeys) if (ts < cutoff) this.seenIdempotencyKeys.delete(k);
1481
- while (this.seenIdempotencyKeys.size >= MAX_IDEMPOTENCY_KEYS) {
1482
- const oldest = this.seenIdempotencyKeys.keys().next().value;
1483
- if (!oldest) break;
1484
- this.seenIdempotencyKeys.delete(oldest);
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
@@ -48,12 +48,18 @@ declare class AgentApiTransport {
48
48
  * @param extra.retry Whether to retry once on transient failures. Safe reads
49
49
  * (GET/HEAD/OPTIONS) default to true; mutations default to false. Set true
50
50
  * only when the endpoint's server-side contract is explicitly idempotent.
51
+ * @param extra.retryStatuses Narrow the HTTP statuses that trigger the retry
52
+ * (default 500/502/503/504). Network errors and timeouts are always retried
53
+ * when `retry` is on. Pass a subset when a status in the default set is a
54
+ * deterministic answer for this endpoint (e.g. a 502 that means "upstream
55
+ * denied", which a retry cannot change).
51
56
  * @param extra.signal Optional caller cancellation combined with the client's
52
57
  * own timeout budget. Aborting either signal cancels the request.
53
58
  */
54
59
  request<T>(path: string, options?: RequestInit, extra?: {
55
60
  timeoutMs?: number;
56
61
  retry?: boolean;
62
+ retryStatuses?: ReadonlySet<number>;
57
63
  signal?: AbortSignal;
58
64
  }): Promise<T>;
59
65
  }
@@ -82,7 +88,37 @@ interface ConnectProviderAccounts {
82
88
  provider: string;
83
89
  accounts: ConnectProviderAccount[];
84
90
  }
91
+ /**
92
+ * One AWS CLI profile an agent may use (`GET /agent/connect/aws/accounts`).
93
+ * `roleArn` is null for a connection's Direct identity. Carries no secret
94
+ * material; sessions are minted per call via `getAwsSession`.
95
+ */
96
+ interface AwsProfile {
97
+ profile: string;
98
+ roleArn: string | null;
99
+ accountId: string;
100
+ region: string;
101
+ label: string | null;
102
+ connectionId: string;
103
+ accountIdentifier: string;
104
+ displayName: string | null;
105
+ connectedAt: string;
106
+ }
107
+ /** AWS CLI `credential_process` output (Version 1), as minted by Connect. */
108
+ interface AwsSessionCredentials {
109
+ Version: 1;
110
+ AccessKeyId: string;
111
+ SecretAccessKey: string;
112
+ SessionToken: string;
113
+ /** ISO 8601 expiry. */
114
+ Expiration: string;
115
+ }
85
116
  declare class ConnectCredentialsApi extends ApiBase {
117
+ /** Platform application identity for private, local Telegram login. Never expose this to tools. */
118
+ telegramGetApplication(): Promise<{
119
+ apiId: number;
120
+ apiHash: string;
121
+ }>;
86
122
  /**
87
123
  * Discover all active Connections visible to this agent for one provider.
88
124
  * Preserve the provider-specific credential projection without guessing a
@@ -476,7 +512,9 @@ declare class ConnectCredentialsApi extends ApiBase {
476
512
  * endpoint walks the agent's full visible scope chain to find a matching
477
513
  * connection (works for inherited team/project Microsoft connections).
478
514
  */
479
- refreshMicrosoftAccountToken(accountIdentifier: string): Promise<{
515
+ refreshMicrosoftAccountToken(accountIdentifier: string, options?: {
516
+ connectionId?: string;
517
+ }): Promise<{
480
518
  accessToken: string;
481
519
  accessTokenExpiresAt: string;
482
520
  expiresAt: string;
@@ -691,12 +729,33 @@ declare class ConnectCredentialsApi extends ApiBase {
691
729
  * the fresh `accessJwt`) — callers typically ignore it and re-fetch via
692
730
  * `getSocialAccounts` for a consistent shape.
693
731
  */
694
- refreshSocialAccount(provider: string, accountIdentifier: string): Promise<{
732
+ refreshSocialAccount(provider: string, accountIdentifier: string, options?: {
733
+ connectionId?: string;
734
+ }): Promise<{
695
735
  accountIdentifier: string;
696
736
  accessToken: string;
697
737
  accessTokenExpiresAt: string;
698
738
  expiresAt: string;
699
739
  }>;
740
+ /**
741
+ * List the AWS CLI profiles visible to this agent across every in-scope AWS
742
+ * connection. Each entry names the `connectionId` + `profile` pair that
743
+ * {@link getAwsSession} mints a session for. Fields are selected explicitly;
744
+ * the wire row is never spread.
745
+ */
746
+ getAwsProfiles(): Promise<AwsProfile[]>;
747
+ /**
748
+ * Mint short-lived AWS session credentials for one profile of one AWS
749
+ * connection, in the AWS CLI `credential_process` (Version 1) shape. The
750
+ * server re-checks scope + active status on every call (403 otherwise; 404
751
+ * for an unknown profile; 403 `aws_access_denied` when AWS denies the
752
+ * assume, 502 on older servers). Minting has no persistent side effect, so
753
+ * one retry is safe, but only on genuinely transient failures (500/503/504
754
+ * and network errors): a denial is deterministic and a retry just doubles
755
+ * the latency of every failing `aws` call. The response is secret material:
756
+ * never log it.
757
+ */
758
+ getAwsSession(connectionId: string, profile: string): Promise<AwsSessionCredentials>;
700
759
  }
701
760
  //# sourceMappingURL=connect-credentials.d.ts.map
702
761
  //#endregion
@@ -1392,17 +1451,34 @@ declare class IdentityApi extends ApiBase {
1392
1451
  platformId: string;
1393
1452
  kind?: "user" | "agent" | "service" | "bot" | "workspace";
1394
1453
  displayName?: string;
1454
+ /**
1455
+ * Provider `email` only: the sender address, matched read-only against an
1456
+ * existing identity's verified email contact. Ignored for other providers.
1457
+ */
1458
+ email?: string;
1459
+ /**
1460
+ * Provider `email` only: `true` when this agent verified the sender
1461
+ * (SPF/DKIM/DMARC). Unverified email senders are never matched to an
1462
+ * identity. Ignored for other providers.
1463
+ */
1464
+ emailVerified?: boolean;
1395
1465
  }): Promise<{
1396
1466
  identityId: string | null;
1397
1467
  status: string;
1398
1468
  created?: boolean;
1399
1469
  reason?: string;
1400
1470
  /**
1401
- * Flattened auriclabs permission strings for the resolved identity
1402
- * (scope-prefixed where applicable). Empty array on miss / org service
1403
- * outage — the runtime gate fails closed in that case.
1471
+ * Flattened auriclabs permission strings that apply at the calling
1472
+ * agent's own scope (`agent:<agentId>`, plus unscoped entries). A sender
1473
+ * with no identity still receives the tenant's `principalKind="all"`
1474
+ * grants. Empty when nothing applies, or when `permissionsUnavailable`.
1404
1475
  */
1405
1476
  permissions: string[];
1477
+ /**
1478
+ * `true` when the org service could not resolve permissions. The empty
1479
+ * `permissions` is then an outage, not a denial: retry rather than deny.
1480
+ */
1481
+ permissionsUnavailable?: boolean;
1406
1482
  }>;
1407
1483
  searchIdentities(args?: {
1408
1484
  q?: string;
@@ -1632,6 +1708,48 @@ declare class WebhooksApi extends ApiBase {
1632
1708
  }
1633
1709
  //# sourceMappingURL=webhooks.d.ts.map
1634
1710
  //#endregion
1711
+ //#region src/domains/wallet.d.ts
1712
+ interface AgentWalletAccount {
1713
+ connectionId: string;
1714
+ chainType: "ethereum" | "solana";
1715
+ address: string;
1716
+ }
1717
+ type AgentWalletTransferStatus = "pending_approval" | "processing" | "succeeded" | "rejected" | "failed";
1718
+ interface AgentWalletTransfer {
1719
+ requestId: string;
1720
+ agentId: string;
1721
+ connectionId: string;
1722
+ chainType: "ethereum" | "solana";
1723
+ network: string;
1724
+ fromAddress: string;
1725
+ toAddress: string;
1726
+ amount: string;
1727
+ reason?: string;
1728
+ status: AgentWalletTransferStatus;
1729
+ providerStatus?: string;
1730
+ providerActionId?: string;
1731
+ failureReason?: string;
1732
+ billingStatus: "unmetered" | "metered" | "failed";
1733
+ createdAt: string;
1734
+ updatedAt: string;
1735
+ decidedAt?: string;
1736
+ }
1737
+ declare class WalletApi extends ApiBase {
1738
+ listWalletAccounts(): Promise<AgentWalletAccount[]>;
1739
+ /** Create a proposal only; a human approves it in the Alfe dashboard. */
1740
+ requestWalletTransfer(input: {
1741
+ connectionId: string;
1742
+ chainType: "ethereum" | "solana";
1743
+ network: "ethereum" | "base" | "arbitrum" | "optimism" | "solana";
1744
+ fromAddress: string;
1745
+ toAddress: string;
1746
+ amount: string;
1747
+ reason?: string;
1748
+ }): Promise<AgentWalletTransfer>;
1749
+ getWalletTransfer(requestId: string): Promise<AgentWalletTransfer>;
1750
+ }
1751
+ //# sourceMappingURL=wallet.d.ts.map
1752
+ //#endregion
1635
1753
  //#region src/domains/chat.d.ts
1636
1754
  declare class ChatApi extends ApiBase {
1637
1755
  ensureDirectConversation(identityId: string): Promise<{
@@ -2003,7 +2121,7 @@ declare class TeamsApi extends ApiBase {
2003
2121
 
2004
2122
  //#endregion
2005
2123
  //#region src/index.d.ts
2006
- interface AgentApiClient extends SyncApi, IntegrationsApi, WorkspaceApi, ConnectCredentialsApi, TeamsApi, ChatApi, SecretsApi, IdentityApi, MemoryApi, SearchApi, KnowledgeApi, DatabaseApi, MobileApi, RemoteApi, SelfApi, VoiceApi, ImagesApi, WebhooksApi {}
2124
+ interface AgentApiClient extends SyncApi, IntegrationsApi, WorkspaceApi, ConnectCredentialsApi, TeamsApi, ChatApi, SecretsApi, IdentityApi, MemoryApi, SearchApi, KnowledgeApi, DatabaseApi, MobileApi, RemoteApi, SelfApi, VoiceApi, ImagesApi, WebhooksApi, WalletApi {}
2007
2125
  declare class AgentApiClient extends ApiBase {
2008
2126
  constructor(config: AgentApiClientConfig);
2009
2127
  }
@@ -2396,12 +2514,10 @@ declare class ClaudeCodeHost {
2396
2514
  /** Alfe sessionId → claude session id map (mirrors ~/.alfe/claude-code/sessions.json). */
2397
2515
  private readonly claudeSessionIds;
2398
2516
  private claudeSessionsPath;
2399
- /** RPC sessionKey → currently RUNNING request id (for chat.abort targeting). */
2517
+ /** Session → one running turn's wire identity and owned child. */
2400
2518
  private readonly runningTurns;
2401
- /** idempotencyKey → settled request id, for E3 duplicate-RPC dedupe. */
2402
- private readonly seenIdempotencyKeys;
2403
- /** RPC sessionKey → resolved SessionDriver so chat.abort can SIGINT the child. */
2404
- private activeDriverBySession;
2519
+ /** Process-local admission/result replay. In-flight records are never evicted. */
2520
+ private readonly agentOperations;
2405
2521
  private claudeSessionSave;
2406
2522
  private stopping;
2407
2523
  private stopPromise;
@@ -2421,6 +2537,7 @@ declare class ClaudeCodeHost {
2421
2537
  */
2422
2538
  handleRequest(req: RpcRequest): Promise<void>;
2423
2539
  private handleAgent;
2540
+ private deliverAgentOutcome;
2424
2541
  private runAgentTurn;
2425
2542
  /**
2426
2543
  * Run a turn; on a `--resume` failure (stale/missing claude session id) start
package/dist/index.d.ts CHANGED
@@ -48,12 +48,18 @@ declare class AgentApiTransport {
48
48
  * @param extra.retry Whether to retry once on transient failures. Safe reads
49
49
  * (GET/HEAD/OPTIONS) default to true; mutations default to false. Set true
50
50
  * only when the endpoint's server-side contract is explicitly idempotent.
51
+ * @param extra.retryStatuses Narrow the HTTP statuses that trigger the retry
52
+ * (default 500/502/503/504). Network errors and timeouts are always retried
53
+ * when `retry` is on. Pass a subset when a status in the default set is a
54
+ * deterministic answer for this endpoint (e.g. a 502 that means "upstream
55
+ * denied", which a retry cannot change).
51
56
  * @param extra.signal Optional caller cancellation combined with the client's
52
57
  * own timeout budget. Aborting either signal cancels the request.
53
58
  */
54
59
  request<T>(path: string, options?: RequestInit, extra?: {
55
60
  timeoutMs?: number;
56
61
  retry?: boolean;
62
+ retryStatuses?: ReadonlySet<number>;
57
63
  signal?: AbortSignal;
58
64
  }): Promise<T>;
59
65
  }
@@ -82,7 +88,37 @@ interface ConnectProviderAccounts {
82
88
  provider: string;
83
89
  accounts: ConnectProviderAccount[];
84
90
  }
91
+ /**
92
+ * One AWS CLI profile an agent may use (`GET /agent/connect/aws/accounts`).
93
+ * `roleArn` is null for a connection's Direct identity. Carries no secret
94
+ * material; sessions are minted per call via `getAwsSession`.
95
+ */
96
+ interface AwsProfile {
97
+ profile: string;
98
+ roleArn: string | null;
99
+ accountId: string;
100
+ region: string;
101
+ label: string | null;
102
+ connectionId: string;
103
+ accountIdentifier: string;
104
+ displayName: string | null;
105
+ connectedAt: string;
106
+ }
107
+ /** AWS CLI `credential_process` output (Version 1), as minted by Connect. */
108
+ interface AwsSessionCredentials {
109
+ Version: 1;
110
+ AccessKeyId: string;
111
+ SecretAccessKey: string;
112
+ SessionToken: string;
113
+ /** ISO 8601 expiry. */
114
+ Expiration: string;
115
+ }
85
116
  declare class ConnectCredentialsApi extends ApiBase {
117
+ /** Platform application identity for private, local Telegram login. Never expose this to tools. */
118
+ telegramGetApplication(): Promise<{
119
+ apiId: number;
120
+ apiHash: string;
121
+ }>;
86
122
  /**
87
123
  * Discover all active Connections visible to this agent for one provider.
88
124
  * Preserve the provider-specific credential projection without guessing a
@@ -476,7 +512,9 @@ declare class ConnectCredentialsApi extends ApiBase {
476
512
  * endpoint walks the agent's full visible scope chain to find a matching
477
513
  * connection (works for inherited team/project Microsoft connections).
478
514
  */
479
- refreshMicrosoftAccountToken(accountIdentifier: string): Promise<{
515
+ refreshMicrosoftAccountToken(accountIdentifier: string, options?: {
516
+ connectionId?: string;
517
+ }): Promise<{
480
518
  accessToken: string;
481
519
  accessTokenExpiresAt: string;
482
520
  expiresAt: string;
@@ -691,12 +729,33 @@ declare class ConnectCredentialsApi extends ApiBase {
691
729
  * the fresh `accessJwt`) — callers typically ignore it and re-fetch via
692
730
  * `getSocialAccounts` for a consistent shape.
693
731
  */
694
- refreshSocialAccount(provider: string, accountIdentifier: string): Promise<{
732
+ refreshSocialAccount(provider: string, accountIdentifier: string, options?: {
733
+ connectionId?: string;
734
+ }): Promise<{
695
735
  accountIdentifier: string;
696
736
  accessToken: string;
697
737
  accessTokenExpiresAt: string;
698
738
  expiresAt: string;
699
739
  }>;
740
+ /**
741
+ * List the AWS CLI profiles visible to this agent across every in-scope AWS
742
+ * connection. Each entry names the `connectionId` + `profile` pair that
743
+ * {@link getAwsSession} mints a session for. Fields are selected explicitly;
744
+ * the wire row is never spread.
745
+ */
746
+ getAwsProfiles(): Promise<AwsProfile[]>;
747
+ /**
748
+ * Mint short-lived AWS session credentials for one profile of one AWS
749
+ * connection, in the AWS CLI `credential_process` (Version 1) shape. The
750
+ * server re-checks scope + active status on every call (403 otherwise; 404
751
+ * for an unknown profile; 403 `aws_access_denied` when AWS denies the
752
+ * assume, 502 on older servers). Minting has no persistent side effect, so
753
+ * one retry is safe, but only on genuinely transient failures (500/503/504
754
+ * and network errors): a denial is deterministic and a retry just doubles
755
+ * the latency of every failing `aws` call. The response is secret material:
756
+ * never log it.
757
+ */
758
+ getAwsSession(connectionId: string, profile: string): Promise<AwsSessionCredentials>;
700
759
  }
701
760
  //# sourceMappingURL=connect-credentials.d.ts.map
702
761
  //#endregion
@@ -1392,17 +1451,34 @@ declare class IdentityApi extends ApiBase {
1392
1451
  platformId: string;
1393
1452
  kind?: "user" | "agent" | "service" | "bot" | "workspace";
1394
1453
  displayName?: string;
1454
+ /**
1455
+ * Provider `email` only: the sender address, matched read-only against an
1456
+ * existing identity's verified email contact. Ignored for other providers.
1457
+ */
1458
+ email?: string;
1459
+ /**
1460
+ * Provider `email` only: `true` when this agent verified the sender
1461
+ * (SPF/DKIM/DMARC). Unverified email senders are never matched to an
1462
+ * identity. Ignored for other providers.
1463
+ */
1464
+ emailVerified?: boolean;
1395
1465
  }): Promise<{
1396
1466
  identityId: string | null;
1397
1467
  status: string;
1398
1468
  created?: boolean;
1399
1469
  reason?: string;
1400
1470
  /**
1401
- * Flattened auriclabs permission strings for the resolved identity
1402
- * (scope-prefixed where applicable). Empty array on miss / org service
1403
- * outage — the runtime gate fails closed in that case.
1471
+ * Flattened auriclabs permission strings that apply at the calling
1472
+ * agent's own scope (`agent:<agentId>`, plus unscoped entries). A sender
1473
+ * with no identity still receives the tenant's `principalKind="all"`
1474
+ * grants. Empty when nothing applies, or when `permissionsUnavailable`.
1404
1475
  */
1405
1476
  permissions: string[];
1477
+ /**
1478
+ * `true` when the org service could not resolve permissions. The empty
1479
+ * `permissions` is then an outage, not a denial: retry rather than deny.
1480
+ */
1481
+ permissionsUnavailable?: boolean;
1406
1482
  }>;
1407
1483
  searchIdentities(args?: {
1408
1484
  q?: string;
@@ -1632,6 +1708,48 @@ declare class WebhooksApi extends ApiBase {
1632
1708
  }
1633
1709
  //# sourceMappingURL=webhooks.d.ts.map
1634
1710
  //#endregion
1711
+ //#region src/domains/wallet.d.ts
1712
+ interface AgentWalletAccount {
1713
+ connectionId: string;
1714
+ chainType: "ethereum" | "solana";
1715
+ address: string;
1716
+ }
1717
+ type AgentWalletTransferStatus = "pending_approval" | "processing" | "succeeded" | "rejected" | "failed";
1718
+ interface AgentWalletTransfer {
1719
+ requestId: string;
1720
+ agentId: string;
1721
+ connectionId: string;
1722
+ chainType: "ethereum" | "solana";
1723
+ network: string;
1724
+ fromAddress: string;
1725
+ toAddress: string;
1726
+ amount: string;
1727
+ reason?: string;
1728
+ status: AgentWalletTransferStatus;
1729
+ providerStatus?: string;
1730
+ providerActionId?: string;
1731
+ failureReason?: string;
1732
+ billingStatus: "unmetered" | "metered" | "failed";
1733
+ createdAt: string;
1734
+ updatedAt: string;
1735
+ decidedAt?: string;
1736
+ }
1737
+ declare class WalletApi extends ApiBase {
1738
+ listWalletAccounts(): Promise<AgentWalletAccount[]>;
1739
+ /** Create a proposal only; a human approves it in the Alfe dashboard. */
1740
+ requestWalletTransfer(input: {
1741
+ connectionId: string;
1742
+ chainType: "ethereum" | "solana";
1743
+ network: "ethereum" | "base" | "arbitrum" | "optimism" | "solana";
1744
+ fromAddress: string;
1745
+ toAddress: string;
1746
+ amount: string;
1747
+ reason?: string;
1748
+ }): Promise<AgentWalletTransfer>;
1749
+ getWalletTransfer(requestId: string): Promise<AgentWalletTransfer>;
1750
+ }
1751
+ //# sourceMappingURL=wallet.d.ts.map
1752
+ //#endregion
1635
1753
  //#region src/domains/chat.d.ts
1636
1754
  declare class ChatApi extends ApiBase {
1637
1755
  ensureDirectConversation(identityId: string): Promise<{
@@ -2003,7 +2121,7 @@ declare class TeamsApi extends ApiBase {
2003
2121
 
2004
2122
  //#endregion
2005
2123
  //#region src/index.d.ts
2006
- interface AgentApiClient extends SyncApi, IntegrationsApi, WorkspaceApi, ConnectCredentialsApi, TeamsApi, ChatApi, SecretsApi, IdentityApi, MemoryApi, SearchApi, KnowledgeApi, DatabaseApi, MobileApi, RemoteApi, SelfApi, VoiceApi, ImagesApi, WebhooksApi {}
2124
+ interface AgentApiClient extends SyncApi, IntegrationsApi, WorkspaceApi, ConnectCredentialsApi, TeamsApi, ChatApi, SecretsApi, IdentityApi, MemoryApi, SearchApi, KnowledgeApi, DatabaseApi, MobileApi, RemoteApi, SelfApi, VoiceApi, ImagesApi, WebhooksApi, WalletApi {}
2007
2125
  declare class AgentApiClient extends ApiBase {
2008
2126
  constructor(config: AgentApiClientConfig);
2009
2127
  }
@@ -2396,12 +2514,10 @@ declare class ClaudeCodeHost {
2396
2514
  /** Alfe sessionId → claude session id map (mirrors ~/.alfe/claude-code/sessions.json). */
2397
2515
  private readonly claudeSessionIds;
2398
2516
  private claudeSessionsPath;
2399
- /** RPC sessionKey → currently RUNNING request id (for chat.abort targeting). */
2517
+ /** Session → one running turn's wire identity and owned child. */
2400
2518
  private readonly runningTurns;
2401
- /** idempotencyKey → settled request id, for E3 duplicate-RPC dedupe. */
2402
- private readonly seenIdempotencyKeys;
2403
- /** RPC sessionKey → resolved SessionDriver so chat.abort can SIGINT the child. */
2404
- private activeDriverBySession;
2519
+ /** Process-local admission/result replay. In-flight records are never evicted. */
2520
+ private readonly agentOperations;
2405
2521
  private claudeSessionSave;
2406
2522
  private stopping;
2407
2523
  private stopPromise;
@@ -2421,6 +2537,7 @@ declare class ClaudeCodeHost {
2421
2537
  */
2422
2538
  handleRequest(req: RpcRequest): Promise<void>;
2423
2539
  private handleAgent;
2540
+ private deliverAgentOutcome;
2424
2541
  private runAgentTurn;
2425
2542
  /**
2426
2543
  * 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.12",
3
+ "version": "0.1.14",
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",
@@ -19,7 +19,7 @@
19
19
  "dist"
20
20
  ],
21
21
  "dependencies": {
22
- "@alfe.ai/agent-api-client": "^0.19.0",
22
+ "@alfe.ai/agent-api-client": "^0.20.0",
23
23
  "@alfe.ai/chat": "^0.0.19",
24
24
  "@alfe.ai/config": "^0.4.1"
25
25
  },