first-tree-staging 0.5.7-staging.366.1 → 0.5.7-staging.367.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.
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { $ as documentContextSchema, At as loadAgents, B as printResults, Bt as SdkError, Ct as agentConfigSchema, Dt as defaultHome, Et as defaultDataDir, F as checkBackgroundService, Ft as channelConfig, G as isServiceUnitDriftDetected, Gt as probeCapabilities, H as getClientServiceStatus, Ht as buildMessageDocumentSnapshots, I as checkClientConfig, It as resetConfig, J as startClientService, Jt as configureClientLoggerForService, Kt as setCliBinding, L as checkNodeVersion, Lt as ClientOrgMismatchError, M as createApiNameResolver, Mt as resetConfigMeta, N as migrateLocalAgentDirs, Nt as resolveConfigReadonly, Ot as getConfigValue, P as checkAgentConfigs, Pt as setConfigValue, Q as canonicalizeResourceRepoUrl, R as checkServerReachable, Rt as ClientUserMismatchError, Tt as defaultConfigDir, U as installClientService, Ut as cleanWorkspaces, V as reconcileAgentConfigs, Vt as SessionRegistry, W as isServiceSupported, Wt as discoverClaudeCodeSkills, Y as stopClientService, Z as ClientRuntime, _ as uploadClientCapabilities, _t as removeLocalAgent, a as declineUpdate, at as ensureFreshAccessToken, b as promptMissingFields, bt as print, c as PACKAGE_NAME, d as fetchServerCommandVersion, dt as saveCredentials, et as findAssembledBriefingFingerprint, f as installGlobalLatest, ft as cliFetch, g as uploadAgentSkills, gt as formatStaleReason, h as reconcileLocalRuntimeProviders, ht as findStaleAliases, i as createExecuteUpdate, jt as readConfigFile, kt as initConfig, l as detectInstallMode, lt as resolveServerUrl, m as listPinnedAgents, mt as COMMAND_VERSION, n as decodeJwtPayload, nt as rotateClientIdWithBackup, o as promptUpdate, ot as ensureFreshAdminToken, p as installGlobalSpec, pt as CLI_USER_AGENT, q as restartClientService, qt as applyClientLoggerConfig, r as deriveHubUrlFromToken, s as refreshServerUpdateTarget, st as loadCredentials, t as HubUrlDerivationError, tt as handleClientOrgMismatch, u as fetchLatestVersion, ut as saveAgentConfig, wt as clientConfigSchema, xt as setJsonMode, y as promptAddAgent, yt as isJsonMode, z as checkWebSocket, zt as FirstTreeHubSDK } from "../connect-token-B-RM1nMS.mjs";
2
+ import { $ as documentContextSchema, At as loadAgents, B as printResults, Bt as SdkError, Ct as agentConfigSchema, Dt as defaultHome, Et as defaultDataDir, F as checkBackgroundService, Ft as channelConfig, G as isServiceUnitDriftDetected, Gt as probeCapabilities, H as getClientServiceStatus, Ht as buildMessageDocumentSnapshots, I as checkClientConfig, It as resetConfig, J as startClientService, Jt as configureClientLoggerForService, Kt as setCliBinding, L as checkNodeVersion, Lt as ClientOrgMismatchError, M as createApiNameResolver, Mt as resetConfigMeta, N as migrateLocalAgentDirs, Nt as resolveConfigReadonly, Ot as getConfigValue, P as checkAgentConfigs, Pt as setConfigValue, Q as canonicalizeResourceRepoUrl, R as checkServerReachable, Rt as ClientUserMismatchError, Tt as defaultConfigDir, U as installClientService, Ut as cleanWorkspaces, V as reconcileAgentConfigs, Vt as SessionRegistry, W as isServiceSupported, Wt as discoverClaudeCodeSkills, Y as stopClientService, Z as ClientRuntime, _ as uploadClientCapabilities, _t as removeLocalAgent, a as declineUpdate, at as ensureFreshAccessToken, b as promptMissingFields, bt as print, c as PACKAGE_NAME, d as fetchServerCommandVersion, dt as saveCredentials, et as findAssembledBriefingFingerprint, f as installGlobalLatest, ft as cliFetch, g as uploadAgentSkills, gt as formatStaleReason, h as reconcileLocalRuntimeProviders, ht as findStaleAliases, i as createExecuteUpdate, jt as readConfigFile, kt as initConfig, l as detectInstallMode, lt as resolveServerUrl, m as listPinnedAgents, mt as COMMAND_VERSION, n as decodeJwtPayload, nt as rotateClientIdWithBackup, o as promptUpdate, ot as ensureFreshAdminToken, p as installGlobalSpec, pt as CLI_USER_AGENT, q as restartClientService, qt as applyClientLoggerConfig, r as deriveHubUrlFromToken, s as refreshServerUpdateTarget, st as loadCredentials, t as HubUrlDerivationError, tt as handleClientOrgMismatch, u as fetchLatestVersion, ut as saveAgentConfig, wt as clientConfigSchema, xt as setJsonMode, y as promptAddAgent, yt as isJsonMode, z as checkWebSocket, zt as FirstTreeHubSDK } from "../connect-token-Cd0Z_mdb.mjs";
3
3
  import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
4
4
  import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync, unlinkSync } from "node:fs";
5
5
  import "node:crypto";
@@ -14401,20 +14401,22 @@ const createClaudeCodeHandler = (config) => {
14401
14401
  consumerDone = consumeOutput(sessionCtx);
14402
14402
  }
14403
14403
  /**
14404
- * Single helper for "turn closed → ack the pending inbox message AND
14404
+ * Single helper for "turn closed → finish the pending inbox message AND
14405
14405
  * drop the replay stash". The two operations are paired everywhere a
14406
14406
  * turn finishes (success / sniff-permanent / forward-error / no-result /
14407
14407
  * non-success subtype / MAX_RETRIES / respawn-fail) — folding them into
14408
14408
  * one call keeps the invariant "stash lives only as long as the turn
14409
14409
  * still might need a replay" enforced in one place. Use the raw
14410
- * `markMessagesCompleted(message)` directly for per-message terminal
14410
+ * `finishTurn(message, ...)` directly for per-message terminal
14411
14411
  * failures (e.g. inject's `toSDKUserMessage` catch) where the semantics is
14412
14412
  * "commit this single inbox message, NOT close the active SDK turn".
14413
14413
  */
14414
- function ackTurnClose(sessionCtx) {
14414
+ async function ackTurnClose(sessionCtx, status) {
14415
14415
  const message = pendingAckMessages.shift();
14416
- if (message) sessionCtx.markMessagesCompleted(message);
14417
- else sessionCtx.markCompleted();
14416
+ if (message) await sessionCtx.finishTurn(message, {
14417
+ status,
14418
+ terminal: true
14419
+ });
14418
14420
  markCurrentPendingMessageConsumed(sessionCtx);
14419
14421
  stashedSdkMessage = null;
14420
14422
  }
@@ -14440,7 +14442,11 @@ const createClaudeCodeHandler = (config) => {
14440
14442
  pushPendingAckMessage(message, sessionCtx);
14441
14443
  } catch (err) {
14442
14444
  sessionCtx.log(`toSDKUserMessage errored: ${err instanceof Error ? err.message : String(err)}`);
14443
- sessionCtx.markMessagesCompleted(message);
14445
+ await sessionCtx.finishTurn(message, {
14446
+ status: "error",
14447
+ terminal: true,
14448
+ errorKind: "deterministic"
14449
+ });
14444
14450
  }
14445
14451
  }
14446
14452
  function scheduleInjectedMessagesDrain(sessionCtx, sessionId) {
@@ -14568,9 +14574,8 @@ const createClaudeCodeHandler = (config) => {
14568
14574
  while (true) {
14569
14575
  if (!currentQuery) return;
14570
14576
  try {
14571
- sessionCtx.setRuntimeState("working");
14572
14577
  for await (const message of currentQuery) {
14573
- sessionCtx.touch();
14578
+ sessionCtx.recordProviderActivity();
14574
14579
  toolCallProcessor.onMessage(message);
14575
14580
  const authFailure = detectClaudeAuthFailure(message);
14576
14581
  if (authFailure && !authHintEmitted) {
@@ -14614,7 +14619,7 @@ const createClaudeCodeHandler = (config) => {
14614
14619
  kind: "turn_end",
14615
14620
  payload: { status: "error" }
14616
14621
  });
14617
- ackTurnClose(sessionCtx);
14622
+ await ackTurnClose(sessionCtx, "error");
14618
14623
  } else {
14619
14624
  retryCount = 0;
14620
14625
  try {
@@ -14624,7 +14629,7 @@ const createClaudeCodeHandler = (config) => {
14624
14629
  kind: "turn_end",
14625
14630
  payload: { status: "success" }
14626
14631
  });
14627
- ackTurnClose(sessionCtx);
14632
+ await ackTurnClose(sessionCtx, "success");
14628
14633
  } catch (err) {
14629
14634
  const reason = err instanceof Error ? err.message : String(err);
14630
14635
  sessionCtx.log(`Failed to forward result: ${reason}`);
@@ -14641,7 +14646,7 @@ const createClaudeCodeHandler = (config) => {
14641
14646
  payload: { status: "error" }
14642
14647
  });
14643
14648
  retryCount = 0;
14644
- ackTurnClose(sessionCtx);
14649
+ await ackTurnClose(sessionCtx, "error");
14645
14650
  }
14646
14651
  }
14647
14652
  } else {
@@ -14650,7 +14655,7 @@ const createClaudeCodeHandler = (config) => {
14650
14655
  kind: "turn_end",
14651
14656
  payload: { status: "success" }
14652
14657
  });
14653
- ackTurnClose(sessionCtx);
14658
+ await ackTurnClose(sessionCtx, "success");
14654
14659
  }
14655
14660
  else {
14656
14661
  const errors = message.errors ? message.errors.join("; ") : message.subtype;
@@ -14667,13 +14672,11 @@ const createClaudeCodeHandler = (config) => {
14667
14672
  kind: "turn_end",
14668
14673
  payload: { status: "error" }
14669
14674
  });
14670
- ackTurnClose(sessionCtx);
14675
+ await ackTurnClose(sessionCtx, "error");
14671
14676
  }
14672
- sessionCtx.setRuntimeState("idle");
14673
14677
  if (message.subtype === "success") authHintEmitted = false;
14674
14678
  }
14675
14679
  }
14676
- sessionCtx.setRuntimeState("idle");
14677
14680
  return;
14678
14681
  } catch (err) {
14679
14682
  const errMsg = err instanceof Error ? err.message : String(err);
@@ -14704,8 +14707,7 @@ const createClaudeCodeHandler = (config) => {
14704
14707
  } catch (emitErr) {
14705
14708
  sessionCtx.log(`Failed to emit retry-exhaustion error event: ${emitErr instanceof Error ? emitErr.message : String(emitErr)}`);
14706
14709
  }
14707
- sessionCtx.setRuntimeState("error");
14708
- ackTurnClose(sessionCtx);
14710
+ await ackTurnClose(sessionCtx, "error");
14709
14711
  return;
14710
14712
  }
14711
14713
  toolCallProcessor.flush();
@@ -14731,8 +14733,7 @@ const createClaudeCodeHandler = (config) => {
14731
14733
  } catch (emitErr) {
14732
14734
  sessionCtx.log(`Failed to emit auto-resume error event: ${emitErr instanceof Error ? emitErr.message : String(emitErr)}`);
14733
14735
  }
14734
- sessionCtx.setRuntimeState("error");
14735
- ackTurnClose(sessionCtx);
14736
+ await ackTurnClose(sessionCtx, "error");
14736
14737
  return;
14737
14738
  }
14738
14739
  }
@@ -15376,9 +15377,8 @@ var TranscriptTailer = class {
15376
15377
  * is NOT a success. claude was interrupted before reaching idle, so we cannot
15377
15378
  * confirm the work completed. Acking it would silently consume the user's
15378
15379
  * message with no replay path — exactly the bug this function guards. So a
15379
- * timeout reports `turn_end: error`, leaves the inbox entries un-acked (the
15380
- * server redelivers them on reconnect/restart for a genuine retry), and surfaces
15381
- * an `error` runtime state.
15380
+ * timeout reports `turn_end: error` and leaves the inbox entries un-acked (the
15381
+ * server redelivers them on reconnect/restart for a genuine retry).
15382
15382
  *
15383
15383
  * Ack policy otherwise mirrors the SDK handler's `ackTurnClose`: a clean close
15384
15384
  * acks even on a plain forward failure (claude finished; re-running yields the
@@ -15395,8 +15395,7 @@ function resolveTurnDisposition(outcome) {
15395
15395
  return {
15396
15396
  status: turnFailed || forwardFailed || timedOut ? "error" : "success",
15397
15397
  ack: consume,
15398
- forward: consume,
15399
- runtimeState: turnFailed || timedOut ? "error" : "idle"
15398
+ forward: consume
15400
15399
  };
15401
15400
  }
15402
15401
  const TURN_TIMEOUT_MS = 600 * 1e3;
@@ -15596,7 +15595,6 @@ const createClaudeCodeTuiHandler = (config) => {
15596
15595
  async function runTurn(text, sessionCtx, messages) {
15597
15596
  if (!tmuxSessionName || !transcriptTailer) throw new Error("runTurn called before session was prepared");
15598
15597
  sessionCtx.markMessagesConsumed(messages);
15599
- sessionCtx.setRuntimeState("working");
15600
15598
  turnAborted = false;
15601
15599
  const state = { finalTexts: [] };
15602
15600
  let turnFailed = false;
@@ -15606,11 +15604,11 @@ const createClaudeCodeTuiHandler = (config) => {
15606
15604
  try {
15607
15605
  transcriptTailer.drainEntries();
15608
15606
  await pasteText(tmuxSessionName, text);
15609
- sessionCtx.touch();
15607
+ sessionCtx.recordProviderActivity();
15610
15608
  const startTs = Date.now();
15611
15609
  while (Date.now() - startTs < TURN_TIMEOUT_MS) {
15612
15610
  if (turnAborted) break;
15613
- sessionCtx.touch();
15611
+ sessionCtx.recordProviderActivity();
15614
15612
  await drainAndConsume(sessionCtx, state);
15615
15613
  const working = (await capturePane(tmuxSessionName)).includes(WORKING_MARKER);
15616
15614
  if (working) everSawWorking = true;
@@ -15680,12 +15678,14 @@ const createClaudeCodeTuiHandler = (config) => {
15680
15678
  kind: "turn_end",
15681
15679
  payload: { status: disposition.status }
15682
15680
  });
15683
- if (disposition.ack) sessionCtx.markMessagesCompleted(messages);
15681
+ if (disposition.ack) await sessionCtx.finishTurn(messages, {
15682
+ status: disposition.status,
15683
+ terminal: true
15684
+ });
15684
15685
  else {
15685
15686
  const reason = timedOut ? "turn_timeout" : turnAborted ? "turn_aborted" : "turn_retryable";
15686
- sessionCtx.markMessagesRetryable(messages, reason);
15687
+ sessionCtx.retryTurn(messages, reason);
15687
15688
  }
15688
- sessionCtx.setRuntimeState(disposition.runtimeState);
15689
15689
  resetProcessor();
15690
15690
  }
15691
15691
  /**
@@ -15710,7 +15710,11 @@ const createClaudeCodeTuiHandler = (config) => {
15710
15710
  sessionCtx.log(`tui inject formatInboundContent failed: ${err instanceof Error ? err.message : String(err)}`);
15711
15711
  }
15712
15712
  if (inputs.length === 0) {
15713
- sessionCtx.markMessagesCompleted(drained);
15713
+ await sessionCtx.finishTurn(drained, {
15714
+ status: "error",
15715
+ terminal: true,
15716
+ errorKind: "deterministic"
15717
+ });
15714
15718
  return;
15715
15719
  }
15716
15720
  await runTurn(inputs.join("\n\n"), sessionCtx, drained);
@@ -16363,7 +16367,6 @@ const createCodexHandler = (config) => {
16363
16367
  sessionCtx.markMessagesConsumed(messages);
16364
16368
  const abort = new AbortController();
16365
16369
  currentAbort = abort;
16366
- sessionCtx.setRuntimeState("working");
16367
16370
  let finalResponse = "";
16368
16371
  let turnFailed = false;
16369
16372
  let userVisibleEmitted = false;
@@ -16383,7 +16386,7 @@ const createCodexHandler = (config) => {
16383
16386
  const streamed = await activeThread.runStreamed(input, { signal: attemptAbort.signal });
16384
16387
  for await (const event of streamed.events) {
16385
16388
  if (attemptAbort.signal.aborted) break;
16386
- sessionCtx.touch();
16389
+ sessionCtx.recordProviderActivity();
16387
16390
  if (event.type === "thread.started") threadId = event.thread_id;
16388
16391
  else if (event.type === "turn.started") {} else if (event.type === "item.completed") {
16389
16392
  const text = processItem(event.item, sessionCtx);
@@ -16526,8 +16529,10 @@ const createCodexHandler = (config) => {
16526
16529
  kind: "turn_end",
16527
16530
  payload: { status: succeeded ? "success" : "error" }
16528
16531
  });
16529
- sessionCtx.setRuntimeState("idle");
16530
- sessionCtx.markMessagesCompleted(messages);
16532
+ await sessionCtx.finishTurn(messages, {
16533
+ status: succeeded ? "success" : "error",
16534
+ terminal: true
16535
+ });
16531
16536
  if (perTurnUsage) sessionCtx.log(`codex usage chatId=${sessionCtx.chatId} duration_ms=${Date.now() - turnStartedAt} input_tokens=${Math.max(0, perTurnUsage.input_tokens - perTurnUsage.cached_input_tokens)} cached_input_tokens=${perTurnUsage.cached_input_tokens} output_tokens=${perTurnUsage.output_tokens} reasoning_output_tokens=${perTurnUsage.reasoning_output_tokens} status=${succeeded ? "success" : "error"}`);
16532
16537
  scheduleQueuedMessagesDrain();
16533
16538
  }
@@ -16558,7 +16563,11 @@ const createCodexHandler = (config) => {
16558
16563
  sessionCtx.log(`codex inject formatInboundContent failed: ${err instanceof Error ? err.message : String(err)}`);
16559
16564
  }
16560
16565
  if (inputs.length === 0) {
16561
- sessionCtx.markMessagesCompleted(drained);
16566
+ await sessionCtx.finishTurn(drained, {
16567
+ status: "error",
16568
+ terminal: true,
16569
+ errorKind: "deterministic"
16570
+ });
16562
16571
  return;
16563
16572
  }
16564
16573
  await runTurn(inputs.join("\n\n"), sessionCtx, drained);
@@ -16992,7 +17001,7 @@ var Deduplicator = class {
16992
17001
  /**
16993
17002
  * Drop every recorded id that starts with `prefix`. Used by the runtime
16994
17003
  * when a chat is LRU-evicted: the in-flight entries for that chat won't
16995
- * be acked (no handler will run `markCompleted`), and the server will
17004
+ * be acked (no handler will run `finishTurn`), and the server will
16996
17005
  * resend the same `(chatId, messageId)` pairs against a fresh session.
16997
17006
  * Leaving the keys in the dedup set would cause the resend to be
16998
17007
  * mis-classified as a duplicate — and (post-#1-fix) re-acked, which
@@ -17022,6 +17031,361 @@ var Deduplicator = class {
17022
17031
  return this.seen.size;
17023
17032
  }
17024
17033
  };
17034
+ var InboxDeliveryCoordinator = class {
17035
+ config;
17036
+ deduplicator = new Deduplicator(1e3);
17037
+ ledgers = /* @__PURE__ */ new Map();
17038
+ recoveringChats = /* @__PURE__ */ new Map();
17039
+ constructor(config) {
17040
+ this.config = config;
17041
+ }
17042
+ receive(entry) {
17043
+ const chatId = entry.chatId ?? entry.message.chatId;
17044
+ const messageId = entry.message.id;
17045
+ const ledger = this.ledger(chatId);
17046
+ if (ledger.recoveryDebt !== "none") return { kind: "recovering" };
17047
+ const dedupKey = this.dedupKey(chatId, messageId);
17048
+ if (this.deduplicator.isDuplicate(dedupKey)) {
17049
+ const stillInFlight = ledger.entries.some((tracked) => tracked.entryId === entry.id);
17050
+ this.config.log.debug({
17051
+ chatId,
17052
+ messageId,
17053
+ entryId: entry.id,
17054
+ stillInFlight
17055
+ }, "duplicate message observed");
17056
+ if (stillInFlight) return { kind: "duplicate-in-flight" };
17057
+ this.config.log.debug({
17058
+ chatId,
17059
+ messageId,
17060
+ entryId: entry.id
17061
+ }, "duplicate key is not tied to an active entry; reprocessing redelivery");
17062
+ }
17063
+ ledger.entries.push({
17064
+ entryId: entry.id,
17065
+ messageId,
17066
+ dedupKey,
17067
+ phase: "tracked"
17068
+ });
17069
+ ledger.entries.sort((a, b) => a.entryId - b.entryId);
17070
+ this.emitWorkChanged(chatId);
17071
+ return {
17072
+ kind: "deliver",
17073
+ work: {
17074
+ chatId,
17075
+ entryId: entry.id,
17076
+ messageId
17077
+ }
17078
+ };
17079
+ }
17080
+ shouldRecoverBeforeDispatch(chatId, hasHealthyLiveHandler, hasLocalSessionRecord) {
17081
+ const ledger = this.ledgers.get(chatId);
17082
+ if (ledger?.recoveryActivationReady) {
17083
+ ledger.recoveryActivationReady = false;
17084
+ this.cleanupLedger(chatId);
17085
+ return false;
17086
+ }
17087
+ if (ledger?.recoveryDebt === "required" || ledger?.recoveryDebt === "running") return true;
17088
+ return Boolean(this.config.recoverChat) && hasLocalSessionRecord && !hasHealthyLiveHandler;
17089
+ }
17090
+ async recoverIfNeeded(chatId, reason) {
17091
+ await this.requestRecovery(chatId, reason);
17092
+ }
17093
+ async requestRecovery(chatId, reason) {
17094
+ const existing = this.recoveringChats.get(chatId);
17095
+ if (existing) {
17096
+ await existing;
17097
+ return;
17098
+ }
17099
+ const recoverChat = this.config.recoverChat;
17100
+ const ledger = this.ledger(chatId);
17101
+ if (!recoverChat) {
17102
+ ledger.recoveryDebt = "required";
17103
+ this.emitWorkChanged(chatId);
17104
+ this.config.log.error({
17105
+ chatId,
17106
+ reason
17107
+ }, "chat requires inbox recovery but no recoverChat callback is configured");
17108
+ return;
17109
+ }
17110
+ ledger.recoveryDebt = "running";
17111
+ this.emitWorkChanged(chatId);
17112
+ let recovery;
17113
+ recovery = recoverChat(chatId).then(() => {
17114
+ this.clearEntriesForRecoverySuccess(chatId);
17115
+ const current = this.ledger(chatId);
17116
+ current.recoveryDebt = "none";
17117
+ current.recoveryActivationReady = true;
17118
+ this.config.log.debug({
17119
+ chatId,
17120
+ reason
17121
+ }, "chat inbox recovery accepted before dispatch");
17122
+ }).catch((err) => {
17123
+ const current = this.ledger(chatId);
17124
+ current.recoveryDebt = "required";
17125
+ this.config.log.warn({
17126
+ chatId,
17127
+ reason,
17128
+ err
17129
+ }, "chat inbox recovery failed before dispatch");
17130
+ }).finally(() => {
17131
+ if (this.recoveringChats.get(chatId) === recovery) this.recoveringChats.delete(chatId);
17132
+ this.emitWorkChanged(chatId);
17133
+ });
17134
+ this.recoveringChats.set(chatId, recovery);
17135
+ await recovery;
17136
+ }
17137
+ async runAdmission(work, op) {
17138
+ const ledger = this.ledger(work.chatId);
17139
+ const next = (ledger.admissionQueue ?? Promise.resolve()).then(op, op);
17140
+ const queueMarker = next.then(() => {}, () => {});
17141
+ ledger.admissionQueue = queueMarker;
17142
+ this.emitWorkChanged(work.chatId);
17143
+ const cleanup = () => {
17144
+ if (ledger.admissionQueue === queueMarker) {
17145
+ ledger.admissionQueue = null;
17146
+ this.cleanupLedger(work.chatId);
17147
+ this.emitWorkChanged(work.chatId);
17148
+ }
17149
+ };
17150
+ next.then(cleanup, cleanup);
17151
+ return next;
17152
+ }
17153
+ markAccepted(work) {
17154
+ const tracked = this.findEntry(work.chatId, work.entryId);
17155
+ if (!tracked) return false;
17156
+ if (tracked.phase === "tracked") {
17157
+ tracked.phase = "accepted";
17158
+ this.emitWorkChanged(work.chatId);
17159
+ }
17160
+ return true;
17161
+ }
17162
+ hasEntry(work) {
17163
+ return this.findEntry(work.chatId, work.entryId) !== null;
17164
+ }
17165
+ markConsumed(chatId, messages) {
17166
+ const ledger = this.ledgers.get(chatId);
17167
+ if (!ledger || ledger.entries.length === 0) return;
17168
+ const consumedIds = this.messageEntryIds(chatId, messages);
17169
+ if (consumedIds.size === 0) return;
17170
+ let changed = false;
17171
+ for (const tracked of ledger.entries) {
17172
+ if (!consumedIds.has(tracked.entryId)) continue;
17173
+ if (tracked.phase === "tracked" || tracked.phase === "accepted") {
17174
+ tracked.phase = "consumed";
17175
+ changed = true;
17176
+ }
17177
+ }
17178
+ if (changed) this.emitWorkChanged(chatId);
17179
+ }
17180
+ async finishTurn(chatId, messages, _outcome) {
17181
+ this.markConsumed(chatId, messages);
17182
+ const throughEntryId = this.lastMessageEntryId(chatId, messages);
17183
+ if (throughEntryId === void 0) {
17184
+ this.config.log.warn({ chatId }, "turn completion ignored because no inboxEntryId was provided");
17185
+ return;
17186
+ }
17187
+ await this.ackThrough(chatId, throughEntryId, "finish_turn", { requireConsumedPrefix: true });
17188
+ }
17189
+ retryTurn(chatId, messages, reason) {
17190
+ const entryIds = this.messageEntryIds(chatId, messages);
17191
+ if (entryIds.size === 0) return;
17192
+ if (!this.ledgers.get(chatId)?.entries.some((entry) => entryIds.has(entry.entryId))) return;
17193
+ this.markRecoveryDebt(chatId, reason);
17194
+ }
17195
+ async prepareSuspend(chatId, reason) {
17196
+ const ledger = this.ledgers.get(chatId);
17197
+ if (!ledger || ledger.entries.length === 0) return;
17198
+ let consumedPrefixCount = 0;
17199
+ for (const tracked of ledger.entries) {
17200
+ if (tracked.phase !== "consumed" && tracked.phase !== "ackPending") break;
17201
+ consumedPrefixCount++;
17202
+ }
17203
+ if (consumedPrefixCount > 0) {
17204
+ const lastConsumed = ledger.entries[consumedPrefixCount - 1];
17205
+ if (lastConsumed) await this.ackThrough(chatId, lastConsumed.entryId, `${reason}:consumed_prefix`, { requireConsumedPrefix: true });
17206
+ }
17207
+ if ((this.ledgers.get(chatId)?.entries ?? []).length > 0) await this.markRecoveryDebt(chatId, reason);
17208
+ }
17209
+ prepareEvict(chatId, reason) {
17210
+ const ledger = this.ledgers.get(chatId);
17211
+ if (!ledger || ledger.entries.length === 0) return;
17212
+ this.markRecoveryDebt(chatId, reason);
17213
+ }
17214
+ async drainForTerminate(chatId) {
17215
+ const ledger = this.ledgers.get(chatId);
17216
+ if (!ledger || ledger.entries.length === 0) return;
17217
+ let acceptedPrefixCount = 0;
17218
+ for (const tracked of ledger.entries) {
17219
+ if (tracked.phase === "tracked") break;
17220
+ acceptedPrefixCount++;
17221
+ }
17222
+ if (acceptedPrefixCount > 0) {
17223
+ const lastAccepted = ledger.entries[acceptedPrefixCount - 1];
17224
+ if (lastAccepted) await this.ackThrough(chatId, lastAccepted.entryId, "terminate");
17225
+ }
17226
+ if ((this.ledgers.get(chatId)?.entries ?? []).length > 0) await this.markRecoveryDebt(chatId, "terminate_unaccepted_remainder");
17227
+ }
17228
+ hasUnsettledWork(chatId) {
17229
+ const ledger = this.ledgers.get(chatId);
17230
+ if (!ledger) return false;
17231
+ return ledger.entries.length > 0 || ledger.recoveryDebt !== "none" || ledger.admissionQueue !== null;
17232
+ }
17233
+ snapshot(chatId) {
17234
+ const ledger = this.ledgers.get(chatId);
17235
+ return {
17236
+ entries: (ledger?.entries ?? []).map((entry) => ({
17237
+ entryId: entry.entryId,
17238
+ messageId: entry.messageId,
17239
+ phase: entry.phase
17240
+ })),
17241
+ recoveryDebt: ledger?.recoveryDebt ?? "none",
17242
+ admissionPending: ledger?.admissionQueue !== null
17243
+ };
17244
+ }
17245
+ async ackThrough(chatId, throughEntryId, reason, opts = {}) {
17246
+ const ledger = this.ledgers.get(chatId);
17247
+ if (!ledger) return;
17248
+ const next = (ledger.ackQueue ?? Promise.resolve()).catch(() => void 0).then(() => this.ackThroughNow(chatId, throughEntryId, reason, opts));
17249
+ const queueMarker = next.then(() => {}, () => {});
17250
+ ledger.ackQueue = queueMarker;
17251
+ this.emitWorkChanged(chatId);
17252
+ try {
17253
+ await next;
17254
+ } finally {
17255
+ if (ledger.ackQueue === queueMarker) {
17256
+ ledger.ackQueue = null;
17257
+ this.cleanupLedger(chatId);
17258
+ this.emitWorkChanged(chatId);
17259
+ }
17260
+ }
17261
+ }
17262
+ async ackThroughNow(chatId, throughEntryId, reason, opts) {
17263
+ const ledger = this.ledgers.get(chatId);
17264
+ if (!ledger || ledger.entries.length === 0) return;
17265
+ if (ledger.recoveryDebt !== "none") {
17266
+ this.config.log.debug({
17267
+ chatId,
17268
+ throughEntryId,
17269
+ reason
17270
+ }, "ACK-through deferred because chat recovery is required");
17271
+ return;
17272
+ }
17273
+ const index = ledger.entries.findIndex((tracked) => tracked.entryId === throughEntryId);
17274
+ if (index < 0) {
17275
+ this.config.log.warn({
17276
+ chatId,
17277
+ throughEntryId,
17278
+ reason
17279
+ }, "attempt completion ignored for untracked inbox entry");
17280
+ return;
17281
+ }
17282
+ const ackPrefix = ledger.entries.slice(0, index + 1);
17283
+ if (opts.requireConsumedPrefix && ackPrefix.some((tracked) => tracked.phase === "tracked" || tracked.phase === "accepted")) {
17284
+ this.config.log.warn({
17285
+ chatId,
17286
+ throughEntryId,
17287
+ reason,
17288
+ prefix: ackPrefix.map((entry) => ({
17289
+ entryId: entry.entryId,
17290
+ phase: entry.phase
17291
+ }))
17292
+ }, "ACK-through blocked because delivery prefix has unconsumed entries");
17293
+ await this.markRecoveryDebt(chatId, `${reason}:unconsumed_prefix_gap`);
17294
+ return;
17295
+ }
17296
+ let changed = false;
17297
+ for (const tracked of ackPrefix) if (tracked.phase !== "ackPending") {
17298
+ tracked.phase = "ackPending";
17299
+ changed = true;
17300
+ }
17301
+ if (changed) this.emitWorkChanged(chatId);
17302
+ try {
17303
+ await this.config.ackEntry(throughEntryId);
17304
+ } catch (err) {
17305
+ this.config.log.warn({
17306
+ chatId,
17307
+ entryId: throughEntryId,
17308
+ reason,
17309
+ err
17310
+ }, "ACK-through failed; retaining ledger");
17311
+ const current = this.ledgers.get(chatId);
17312
+ if (current) {
17313
+ for (const tracked of current.entries) if (tracked.entryId <= throughEntryId && tracked.phase === "ackPending") tracked.phase = "consumed";
17314
+ }
17315
+ this.emitWorkChanged(chatId);
17316
+ this.markRecoveryDebt(chatId, `${reason}:ack_failed`);
17317
+ return;
17318
+ }
17319
+ const current = this.ledgers.get(chatId);
17320
+ if (!current) return;
17321
+ const committed = current.entries.filter((tracked) => tracked.entryId <= throughEntryId);
17322
+ current.entries = current.entries.filter((tracked) => tracked.entryId > throughEntryId);
17323
+ for (const tracked of committed) this.deduplicator.drop(tracked.dedupKey);
17324
+ if (current.entries.length === 0 && current.recoveryDebt === "required") current.recoveryDebt = "none";
17325
+ this.cleanupLedger(chatId);
17326
+ this.emitWorkChanged(chatId);
17327
+ }
17328
+ async markRecoveryDebt(chatId, reason) {
17329
+ const ledger = this.ledger(chatId);
17330
+ if (ledger.recoveryDebt !== "required") {
17331
+ ledger.recoveryDebt = "required";
17332
+ this.emitWorkChanged(chatId);
17333
+ }
17334
+ this.config.log.warn({
17335
+ chatId,
17336
+ reason,
17337
+ entryIds: ledger.entries.map((entry) => entry.entryId)
17338
+ }, "chat has unsettled inbox work; waiting for recovery redelivery");
17339
+ await this.requestRecovery(chatId, reason);
17340
+ }
17341
+ clearEntriesForRecoverySuccess(chatId) {
17342
+ const ledger = this.ledger(chatId);
17343
+ for (const tracked of ledger.entries) this.deduplicator.drop(tracked.dedupKey);
17344
+ ledger.entries = [];
17345
+ }
17346
+ findEntry(chatId, entryId) {
17347
+ return this.ledgers.get(chatId)?.entries.find((entry) => entry.entryId === entryId) ?? null;
17348
+ }
17349
+ messageEntryIds(chatId, messages) {
17350
+ const batch = Array.isArray(messages) ? messages : [messages];
17351
+ const entryIds = /* @__PURE__ */ new Set();
17352
+ for (const message of batch) if (message.chatId === chatId && message.inboxEntryId !== void 0) entryIds.add(message.inboxEntryId);
17353
+ return entryIds;
17354
+ }
17355
+ lastMessageEntryId(chatId, messages) {
17356
+ const batch = Array.isArray(messages) ? messages : [messages];
17357
+ let throughEntryId;
17358
+ for (const message of batch) {
17359
+ if (message.chatId !== chatId) continue;
17360
+ if (message.inboxEntryId !== void 0) throughEntryId = message.inboxEntryId;
17361
+ }
17362
+ return throughEntryId;
17363
+ }
17364
+ ledger(chatId) {
17365
+ const existing = this.ledgers.get(chatId);
17366
+ if (existing) return existing;
17367
+ const ledger = {
17368
+ entries: [],
17369
+ recoveryDebt: "none",
17370
+ recoveryActivationReady: false,
17371
+ admissionQueue: null,
17372
+ ackQueue: null
17373
+ };
17374
+ this.ledgers.set(chatId, ledger);
17375
+ return ledger;
17376
+ }
17377
+ cleanupLedger(chatId) {
17378
+ const ledger = this.ledgers.get(chatId);
17379
+ if (!ledger) return;
17380
+ if (ledger.entries.length === 0 && ledger.recoveryDebt === "none" && !ledger.recoveryActivationReady && ledger.admissionQueue === null && ledger.ackQueue === null) this.ledgers.delete(chatId);
17381
+ }
17382
+ emitWorkChanged(chatId) {
17383
+ this.config.onWorkChanged(chatId);
17384
+ }
17385
+ dedupKey(chatId, messageId) {
17386
+ return `${chatId}:${messageId}`;
17387
+ }
17388
+ };
17025
17389
  /**
17026
17390
  * Sanitize an `err.message` (or any free-text preview) before it leaves the
17027
17391
  * `safe to embed in logs but NOT chat` boundary established by
@@ -17848,9 +18212,9 @@ var SessionManager = class {
17848
18212
  sessions = /* @__PURE__ */ new Map();
17849
18213
  evictedMappings = /* @__PURE__ */ new Map();
17850
18214
  config;
18215
+ inboxDelivery;
17851
18216
  /** Last lazy Context-Tree re-resolution attempt (epoch ms); see `TREE_RERESOLVE_INTERVAL_MS`. */
17852
18217
  lastTreeResolveAttemptAt = 0;
17853
- deduplicator = new Deduplicator(1e3);
17854
18218
  /**
17855
18219
  * Current trigger (messageId + senderId) per chat — the message that kicked
17856
18220
  * off the current or most-recent turn. Read by `forwardResult` (via the
@@ -17860,38 +18224,6 @@ var SessionManager = class {
17860
18224
  */
17861
18225
  currentTrigger = /* @__PURE__ */ new Map();
17862
18226
  registry;
17863
- /**
17864
- * In-flight inbox entries per chat — delivered entries that this process
17865
- * has handed toward a handler and has not yet committed. Completion is
17866
- * identity based: handlers report the concrete SessionMessage or fused
17867
- * batch they actually consumed, and the runtime sends one ack-through for
17868
- * that batch's last inbox entry.
17869
- *
17870
- * Kept here (not on `SessionEntry`) because dispatch may push an entryId
17871
- * before any session record exists for the chat (`startNewSession` runs
17872
- * `routeMessage` first), and the queue must survive a session being
17873
- * suspended / evicted between dispatch and markCompleted.
17874
- *
17875
- * Sized small in practice: usually 1, briefly up to N when several
17876
- * messages land while a turn is mid-flight. Entries that were delivered
17877
- * but only queued inside a handler remain tracked until the handler's
17878
- * actual consuming turn calls `markMessagesCompleted`.
17879
- * See
17880
- * docs/inflight-message-recovery-design.md §4.
17881
- */
17882
- inFlightEntries = /* @__PURE__ */ new Map();
17883
- /**
17884
- * Per-chat admission barrier. It serializes the pre-handler admission phase
17885
- * only: config refresh, eager asset fetch, marking the entry accepted, and
17886
- * invoking `routeMessage()`. It deliberately does NOT wait for the handler's
17887
- * turn promise to settle, so A2 can still be appended/injected while A1's
17888
- * attempt is running; it just cannot overtake A1 before A1 reaches handler
17889
- * membership.
17890
- */
17891
- admissionQueues = /* @__PURE__ */ new Map();
17892
- recoveringChats = /* @__PURE__ */ new Map();
17893
- recoveryActivationReady = /* @__PURE__ */ new Set();
17894
- requiresInboxRecovery = /* @__PURE__ */ new Set();
17895
18227
  pendingQueue = [];
17896
18228
  lastReportedStates = /* @__PURE__ */ new Map();
17897
18229
  sessionRuntimeStates = /* @__PURE__ */ new Map();
@@ -17901,6 +18233,12 @@ var SessionManager = class {
17901
18233
  _activeCount = 0;
17902
18234
  constructor(config) {
17903
18235
  this.config = config;
18236
+ this.inboxDelivery = new InboxDeliveryCoordinator({
18237
+ ackEntry: config.ackEntry,
18238
+ recoverChat: config.recoverChat,
18239
+ onWorkChanged: (chatId) => this.projectSessionRuntime(chatId),
18240
+ log: config.log
18241
+ });
17904
18242
  this.registry = config.registryPath ? new SessionRegistry(config.registryPath) : null;
17905
18243
  this.idleTimer = setInterval(() => this.evictIdle(), 1e4);
17906
18244
  if (config.onSessionRuntimeChange) {
@@ -17916,7 +18254,7 @@ var SessionManager = class {
17916
18254
  }
17917
18255
  /**
17918
18256
  * Dispatch an inbox entry. ACK is deferred until the handler reports a
17919
- * completed turn via `ctx.markMessagesCompleted(...)`.
18257
+ * completed turn via `ctx.finishTurn(...)`.
17920
18258
  *
17921
18259
  * Delayed ACK semantics (post inflight-message-recovery): the entry stays
17922
18260
  * `delivered` server-side until forwardResult succeeds (or the handler
@@ -17933,40 +18271,16 @@ var SessionManager = class {
17933
18271
  async dispatch(entry) {
17934
18272
  const chatId = entry.chatId ?? entry.message.chatId;
17935
18273
  const messageId = entry.message.id;
17936
- if (this.shouldRecoverBeforeDispatch(chatId)) {
17937
- await this.recoverChatBeforeDispatch(chatId, entry.id, messageId);
18274
+ if (this.inboxDelivery.shouldRecoverBeforeDispatch(chatId, this.hasHealthyLiveHandler(chatId) || this.hasPendingTransientRetry(chatId), this.hasLocalSessionRecord(chatId))) {
18275
+ await this.inboxDelivery.recoverIfNeeded(chatId, `before_dispatch:${entry.id}:${messageId}`);
17938
18276
  return;
17939
18277
  }
17940
- const dedupKey = `${chatId}:${messageId}`;
17941
- if (this.deduplicator.isDuplicate(dedupKey)) {
17942
- const queue = this.inFlightEntries.get(chatId);
17943
- const stillInFlight = queue ? queue.some((tracked) => tracked.entryId === entry.id) : false;
17944
- this.config.log.debug({
17945
- chatId,
17946
- messageId,
17947
- entryId: entry.id,
17948
- stillInFlight
17949
- }, "duplicate message observed");
17950
- if (stillInFlight) return;
17951
- this.config.log.debug({
17952
- chatId,
17953
- messageId,
17954
- entryId: entry.id
17955
- }, "duplicate key is not tied to an active entry; reprocessing redelivery");
17956
- }
17957
- const queue = this.inFlightEntries.get(chatId);
17958
- const tracked = {
17959
- entryId: entry.id,
17960
- messageId,
17961
- dedupKey,
17962
- accepted: false,
17963
- consumed: false
17964
- };
17965
- if (queue) queue.push(tracked);
17966
- else this.inFlightEntries.set(chatId, [tracked]);
18278
+ const decision = this.inboxDelivery.receive(entry);
18279
+ if (decision.kind !== "deliver") return;
18280
+ const { work } = decision;
17967
18281
  let routePromise;
17968
- await this.withAdmissionBarrier(chatId, async () => {
17969
- if (!this.isTrackedEntry(chatId, entry.id)) return;
18282
+ await this.inboxDelivery.runAdmission(work, async () => {
18283
+ if (!this.inboxDelivery.hasEntry(work)) return;
17970
18284
  if (this.config.agentConfigCache) try {
17971
18285
  await this.config.agentConfigCache.refreshIfNewer(this.config.agentIdentity.agentId, entry.message.configVersion);
17972
18286
  } catch (err) {
@@ -17977,13 +18291,13 @@ var SessionManager = class {
17977
18291
  err
17978
18292
  }, "config version mismatch — skipping refresh");
17979
18293
  }
17980
- if (!this.isTrackedEntry(chatId, entry.id)) return;
18294
+ if (!this.inboxDelivery.hasEntry(work)) return;
17981
18295
  const message = this.extractMessage(entry);
17982
18296
  await this.ensureImagesLocal(message);
17983
- if (!this.markTrackedEntryAccepted(chatId, entry.id)) return;
18297
+ if (!this.inboxDelivery.markAccepted(work)) return;
17984
18298
  if (!this.hasHealthyLiveHandler(chatId)) await this.ensureContextTreeBinding();
17985
18299
  routePromise = this.routeMessage(chatId, message).catch((err) => {
17986
- if (this.isTrackedEntry(chatId, entry.id)) this.clearInFlightForRecovery(chatId, "route_message_failed");
18300
+ if (this.inboxDelivery.hasEntry(work)) this.inboxDelivery.retryTurn(chatId, message, "route_message_failed");
17987
18301
  throw err;
17988
18302
  });
17989
18303
  });
@@ -18048,7 +18362,7 @@ var SessionManager = class {
18048
18362
  this.lastReportedStates.delete(chatId);
18049
18363
  this.currentTrigger.delete(chatId);
18050
18364
  for (let i = this.pendingQueue.length - 1; i >= 0; i--) if (this.pendingQueue[i]?.chatId === chatId) this.pendingQueue.splice(i, 1);
18051
- this.drainAllInFlightEntries(chatId);
18365
+ await this.inboxDelivery.drainForTerminate(chatId);
18052
18366
  this.recomputeRuntimeState();
18053
18367
  this.persistRegistry();
18054
18368
  this.drainPendingQueue();
@@ -18139,92 +18453,33 @@ var SessionManager = class {
18139
18453
  /**
18140
18454
  * Compatibility hook for AgentSlot's bind listener. Bind-time recovery is
18141
18455
  * now server-driven; chat-scoped same-socket recovery is requested lazily
18142
- * by `dispatch` when no healthy live handler exists.
18456
+ * by `dispatch` when a locally held chat has no healthy live handler.
18143
18457
  */
18144
18458
  noteBindRecoveryComplete() {}
18145
- async withAdmissionBarrier(chatId, op) {
18146
- const next = (this.admissionQueues.get(chatId) ?? Promise.resolve()).then(op, op);
18147
- this.admissionQueues.set(chatId, next);
18148
- const cleanup = () => {
18149
- if (this.admissionQueues.get(chatId) === next) this.admissionQueues.delete(chatId);
18150
- };
18151
- next.then(cleanup, cleanup);
18152
- await next;
18153
- }
18154
- isTrackedEntry(chatId, entryId) {
18155
- return this.inFlightEntries.get(chatId)?.some((tracked) => tracked.entryId === entryId) ?? false;
18156
- }
18157
- markTrackedEntryAccepted(chatId, entryId) {
18158
- const tracked = this.inFlightEntries.get(chatId)?.find((entry) => entry.entryId === entryId);
18159
- if (!tracked) return false;
18160
- tracked.accepted = true;
18161
- return true;
18162
- }
18163
18459
  hasHealthyLiveHandler(chatId) {
18164
18460
  const entry = this.sessions.get(chatId);
18165
18461
  return entry?.status === "active" && entry.suspending === null;
18166
18462
  }
18167
- shouldRecoverBeforeDispatch(chatId) {
18168
- if (this.recoveryActivationReady.has(chatId)) {
18169
- this.recoveryActivationReady.delete(chatId);
18170
- return false;
18171
- }
18172
- if (this.requiresInboxRecovery.has(chatId)) return true;
18173
- return Boolean(this.config.recoverChat) && !this.hasHealthyLiveHandler(chatId);
18463
+ hasPendingTransientRetry(chatId) {
18464
+ const entry = this.sessions.get(chatId);
18465
+ return Boolean(entry && entry.retryAttempt > 0);
18174
18466
  }
18175
- async recoverChatBeforeDispatch(chatId, entryId, messageId) {
18176
- const existing = this.recoveringChats.get(chatId);
18177
- if (existing) {
18178
- await existing;
18179
- return;
18180
- }
18181
- const recoverChat = this.config.recoverChat;
18182
- if (!recoverChat) {
18183
- this.config.log.error({
18184
- chatId,
18185
- entryId,
18186
- messageId
18187
- }, "chat requires inbox recovery but no recoverChat callback is configured; deferring dispatch");
18188
- return;
18189
- }
18190
- let recovery;
18191
- recovery = recoverChat(chatId).then(() => {
18192
- this.requiresInboxRecovery.delete(chatId);
18193
- this.recoveryActivationReady.add(chatId);
18194
- this.config.log.debug({
18195
- chatId,
18196
- entryId,
18197
- messageId
18198
- }, "chat inbox recovery accepted before dispatch");
18199
- }).catch((err) => {
18200
- this.config.log.warn({
18201
- chatId,
18202
- entryId,
18203
- messageId,
18204
- err
18205
- }, "chat inbox recovery failed before dispatch");
18206
- }).finally(() => {
18207
- if (this.recoveringChats.get(chatId) === recovery) this.recoveringChats.delete(chatId);
18208
- });
18209
- this.recoveringChats.set(chatId, recovery);
18210
- await recovery;
18467
+ hasLocalSessionRecord(chatId) {
18468
+ return this.sessions.has(chatId) || this.evictedMappings.has(chatId);
18211
18469
  }
18212
18470
  async routeMessage(chatId, message) {
18213
- if (message.id) this.currentTrigger.set(chatId, {
18214
- messageId: message.id,
18215
- senderId: message.senderId
18216
- });
18217
18471
  const existing = this.sessions.get(chatId);
18218
18472
  if (existing && existing.retryAttempt > 0) {
18219
- existing.startMessage = message;
18473
+ existing.retryQueuedMessages.push(message);
18220
18474
  this.triggerImmediateRetry(chatId);
18221
18475
  return;
18222
18476
  }
18223
18477
  if (existing) switch (existing.status) {
18224
18478
  case "active":
18479
+ this.setCurrentTrigger(chatId, message);
18225
18480
  existing.handler.inject(message);
18226
18481
  existing.lastActivity = Date.now();
18227
- this.setSessionRuntimeState(chatId, "working");
18482
+ this.projectSessionRuntime(chatId);
18228
18483
  this.config.log.debug({ chatId }, "message injected");
18229
18484
  return;
18230
18485
  case "suspended":
@@ -18285,13 +18540,16 @@ var SessionManager = class {
18285
18540
  lastRetryReason: null,
18286
18541
  lastRetryRawError: null,
18287
18542
  startMessage: message,
18543
+ retryQueuedMessages: [],
18288
18544
  retryFromEvicted: evicted ?? null
18289
18545
  };
18290
18546
  this.sessions.set(chatId, entry);
18291
18547
  this._activeCount++;
18292
18548
  if (evicted) this.evictedMappings.delete(chatId);
18293
18549
  this.notifySessionState(chatId, "active");
18550
+ this.projectSessionRuntime(chatId);
18294
18551
  try {
18552
+ this.setCurrentTrigger(chatId, message);
18295
18553
  if (evicted) {
18296
18554
  const sessionId = await handler.resume(message, evicted.claudeSessionId, ctx);
18297
18555
  entry.claudeSessionId = sessionId;
@@ -18318,7 +18576,7 @@ var SessionManager = class {
18318
18576
  phase,
18319
18577
  classification
18320
18578
  })) {
18321
- this.drainAllInFlightEntries(chatId);
18579
+ await this.inboxDelivery.drainForTerminate(chatId);
18322
18580
  this.sessions.delete(chatId);
18323
18581
  this.sessionRuntimeStates.delete(chatId);
18324
18582
  this.recomputeRuntimeState();
@@ -18342,7 +18600,9 @@ var SessionManager = class {
18342
18600
  this._activeCount++;
18343
18601
  entry.lastActivity = Date.now();
18344
18602
  this.notifySessionState(entry.chatId, "active");
18603
+ this.projectSessionRuntime(entry.chatId);
18345
18604
  try {
18605
+ if (message) this.setCurrentTrigger(entry.chatId, message);
18346
18606
  entry.claudeSessionId = await entry.handler.resume(message ?? void 0, entry.claudeSessionId, ctx);
18347
18607
  this.config.log.info({
18348
18608
  chatId: entry.chatId,
@@ -18358,7 +18618,7 @@ var SessionManager = class {
18358
18618
  phase: "resume",
18359
18619
  classification
18360
18620
  })) {
18361
- this.drainAllInFlightEntries(entry.chatId);
18621
+ await this.inboxDelivery.drainForTerminate(entry.chatId);
18362
18622
  this.sessions.delete(entry.chatId);
18363
18623
  this.sessionRuntimeStates.delete(entry.chatId);
18364
18624
  this.recomputeRuntimeState();
@@ -18396,6 +18656,7 @@ var SessionManager = class {
18396
18656
  this._activeCount--;
18397
18657
  entry.status = "suspended";
18398
18658
  this.sessionRuntimeStates.delete(chatId);
18659
+ this.projectSessionRuntime(chatId);
18399
18660
  this.recomputeRuntimeState();
18400
18661
  this.config.log.info({
18401
18662
  chatId,
@@ -18437,7 +18698,9 @@ var SessionManager = class {
18437
18698
  }, delayMs);
18438
18699
  return true;
18439
18700
  }
18701
+ entry.status = "errored";
18440
18702
  this.notifySessionState(chatId, "errored");
18703
+ this.projectSessionRuntime(chatId);
18441
18704
  try {
18442
18705
  const preview = redactErrorPreview(errMsg, 800);
18443
18706
  ctx.emitEvent({
@@ -18514,9 +18777,11 @@ var SessionManager = class {
18514
18777
  entry.handler = newHandler;
18515
18778
  const ctx = this.buildSessionContext(chatId);
18516
18779
  this.notifySessionState(chatId, "active");
18780
+ this.projectSessionRuntime(chatId);
18517
18781
  try {
18518
18782
  const resumeMessage = entry.startMessage ?? null;
18519
18783
  const previousSessionId = entry.claudeSessionId || entry.retryFromEvicted?.claudeSessionId || "";
18784
+ if (resumeMessage) this.setCurrentTrigger(chatId, resumeMessage);
18520
18785
  if (previousSessionId) entry.claudeSessionId = await newHandler.resume(resumeMessage ?? void 0, previousSessionId, ctx);
18521
18786
  else {
18522
18787
  const message = resumeMessage ?? buildEmptySessionMessage(chatId);
@@ -18546,6 +18811,7 @@ var SessionManager = class {
18546
18811
  emitErr
18547
18812
  }, "resilience retry_succeeded emit failed");
18548
18813
  }
18814
+ this.drainRetryQueuedMessages(entry);
18549
18815
  this.persistRegistry();
18550
18816
  } catch (err) {
18551
18817
  const classification = classify(err, { source: "session" });
@@ -18556,7 +18822,7 @@ var SessionManager = class {
18556
18822
  phase: previousAvailable(entry) ? "resume" : "start",
18557
18823
  classification
18558
18824
  })) {
18559
- this.drainAllInFlightEntries(chatId);
18825
+ await this.inboxDelivery.drainForTerminate(chatId);
18560
18826
  this.sessions.delete(chatId);
18561
18827
  this.sessionRuntimeStates.delete(chatId);
18562
18828
  this.recomputeRuntimeState();
@@ -18579,13 +18845,40 @@ var SessionManager = class {
18579
18845
  }
18580
18846
  this.runRetry(chatId);
18581
18847
  }
18848
+ drainRetryQueuedMessages(entry) {
18849
+ if (entry.retryQueuedMessages.length === 0) return;
18850
+ const queued = entry.retryQueuedMessages.splice(0);
18851
+ for (const message of queued) {
18852
+ this.setCurrentTrigger(entry.chatId, message);
18853
+ try {
18854
+ entry.handler.inject(message);
18855
+ entry.lastActivity = Date.now();
18856
+ } catch (err) {
18857
+ this.config.log.warn({
18858
+ chatId: entry.chatId,
18859
+ messageId: message.id,
18860
+ err
18861
+ }, "retry queued inject failed");
18862
+ this.inboxDelivery.retryTurn(entry.chatId, message, "retry_queued_inject_failed");
18863
+ break;
18864
+ }
18865
+ }
18866
+ this.projectSessionRuntime(entry.chatId);
18867
+ }
18868
+ setCurrentTrigger(chatId, message) {
18869
+ if (!message.id) return;
18870
+ this.currentTrigger.set(chatId, {
18871
+ messageId: message.id,
18872
+ senderId: message.senderId
18873
+ });
18874
+ }
18582
18875
  /**
18583
18876
  * Try to acquire an active slot. If at concurrency limit:
18584
18877
  * 1. Suspend the least-recently-active session to free a slot
18585
18878
  * 2. If no candidates, queue the message
18586
18879
  *
18587
18880
  * Returns true if slot acquired, false if queued. The in-flight entryId
18588
- * is tracked separately in `inFlightEntries` (populated at dispatch),
18881
+ * is tracked separately in `InboxDeliveryCoordinator` (populated at dispatch),
18589
18882
  * so the queue doesn't carry inbox metadata.
18590
18883
  */
18591
18884
  acquireActiveSlot(chatId, message) {
@@ -18598,7 +18891,10 @@ var SessionManager = class {
18598
18891
  }
18599
18892
  if (oldestActive) {
18600
18893
  this.config.log.info({ chatId: oldestActive.chatId }, "session preempted for concurrency");
18601
- this.suspendSession(oldestActive);
18894
+ this.suspendSession(oldestActive, {
18895
+ reason: "concurrency_preempted",
18896
+ ackConsumedPrefix: false
18897
+ });
18602
18898
  return true;
18603
18899
  }
18604
18900
  this.config.log.info({ chatId }, "concurrency limit reached, queuing");
@@ -18608,14 +18904,21 @@ var SessionManager = class {
18608
18904
  });
18609
18905
  return false;
18610
18906
  }
18611
- suspendSession(entry) {
18612
- this.ackConsumedInFlightForSuspend(entry.chatId);
18613
- this.clearInFlightForRecovery(entry.chatId, "session_suspended_unconsumed_tail");
18907
+ suspendSession(entry, opts = {
18908
+ reason: "session_suspended",
18909
+ ackConsumedPrefix: true
18910
+ }) {
18911
+ const prepare = opts.ackConsumedPrefix ? this.inboxDelivery.prepareSuspend(entry.chatId, opts.reason) : Promise.resolve(this.inboxDelivery.prepareEvict(entry.chatId, opts.reason));
18614
18912
  entry.status = "suspended";
18615
18913
  this._activeCount--;
18616
18914
  this.sessionRuntimeStates.delete(entry.chatId);
18617
18915
  this.recomputeRuntimeState();
18618
- entry.suspending = entry.handler.suspend().catch((err) => {
18916
+ entry.suspending = prepare.then(() => entry.handler.suspend()).catch((err) => {
18917
+ this.config.log.warn({
18918
+ chatId: entry.chatId,
18919
+ err
18920
+ }, "suspend preparation error");
18921
+ }).then(() => void 0).catch((err) => {
18619
18922
  this.config.log.warn({
18620
18923
  chatId: entry.chatId,
18621
18924
  err
@@ -18671,7 +18974,7 @@ var SessionManager = class {
18671
18974
  this.sessions.delete(candidate.key);
18672
18975
  this.sessionRuntimeStates.delete(candidate.key);
18673
18976
  this.currentTrigger.delete(candidate.key);
18674
- this.clearInFlightForRecovery(candidate.key, "session_evicted");
18977
+ this.inboxDelivery.prepareEvict(candidate.key, "session_evicted");
18675
18978
  this.recomputeRuntimeState();
18676
18979
  this.persistRegistry();
18677
18980
  }
@@ -18682,28 +18985,12 @@ var SessionManager = class {
18682
18985
  * Invariants this routine relies on:
18683
18986
  * 1. `lastActivity` is monotonic per session and is bumped by every
18684
18987
  * inbound activity — `dispatch` (new chat / resume), `inject`
18685
- * (mid-turn message), and the handler's `touch()` on each SDK event.
18686
- * 2. `runtimeState` reflects what the agent is currently doing as best
18687
- * the runtime can tell:
18688
- * - `working` — a turn is in flight. Set by the handler at the top
18689
- * of its consume loop AND by `SessionManager` on every `inject`
18690
- * (because the handler can't observe inject from inside the SDK
18691
- * for-await — see #536). Cleared back to `idle` on each
18692
- * `result` message from the SDK.
18693
- * - `blocked` — only reachable if a handler chooses to set it
18694
- * explicitly. The runtime no longer auto-migrates `working` →
18695
- * `blocked` on inactivity: with reasoning models a 2-minute
18696
- * quiet stretch is normal deep-thinking, not a stuck process,
18697
- * and the auto-migration was producing false-positive UI
18698
- * warnings. State kept as a semantic slot for future use
18699
- * (e.g. if/when the SDK transport exposes a real stuck signal).
18700
- * - `idle` — no work in flight.
18701
- * If you add a new way to wake the session up, you MUST also set
18702
- * `runtimeState` to `working` from that path, or this guard will
18703
- * treat the chat as idle and reap it.
18704
- * 3. `working_grace_seconds` is an UPPER bound on how long a `working`
18705
- * / `blocked` chat can hold a slot past `idle_timeout` — defends
18706
- * against a stuck handler that never flips back to `idle`.
18988
+ * (mid-turn message), and the handler's provider-activity callback.
18989
+ * 2. The coordinator is the source of truth for unsettled delivery work.
18990
+ * If a chat has tracked / consumed / ACK-pending entries or recovery
18991
+ * debt, the reaper must not treat it as idle before the hard cap.
18992
+ * 3. `working_grace_seconds` is an UPPER bound on how long unsettled work
18993
+ * can hold a slot past `idle_timeout`.
18707
18994
  */
18708
18995
  evictIdle() {
18709
18996
  const timeoutMs = this.config.session.idle_timeout * 1e3;
@@ -18714,14 +19001,15 @@ var SessionManager = class {
18714
19001
  const inactiveMs = now - session.lastActivity;
18715
19002
  if (inactiveMs <= timeoutMs) continue;
18716
19003
  const currentState = this.sessionRuntimeStates.get(session.chatId);
19004
+ const hasUnsettledWork = this.inboxDelivery.hasUnsettledWork(session.chatId);
18717
19005
  const pastHardCap = inactiveMs >= timeoutMs + workingGraceMs;
18718
- if ((currentState === "working" || currentState === "blocked") && !pastHardCap) {
19006
+ if (hasUnsettledWork && !pastHardCap) {
18719
19007
  this.config.log.info({
18720
19008
  chatId: session.chatId,
18721
19009
  runtimeState: currentState,
18722
19010
  inactiveSec: Math.round(inactiveMs / 1e3),
18723
19011
  graceSec: this.config.session.working_grace_seconds
18724
- }, "session idle threshold reached but still working — skipping suspend");
19012
+ }, "session idle threshold reached but inbox work is unsettled — skipping suspend");
18725
19013
  continue;
18726
19014
  }
18727
19015
  this.config.log.info({
@@ -18748,104 +19036,6 @@ var SessionManager = class {
18748
19036
  this.lastReportedStates.set(chatId, state);
18749
19037
  this.config.onStateChange(chatId, state);
18750
19038
  }
18751
- ackThroughTrackedEntry(chatId, throughEntryId) {
18752
- const queue = this.inFlightEntries.get(chatId);
18753
- if (!queue || queue.length === 0) return;
18754
- const index = queue.findIndex((tracked) => tracked.entryId === throughEntryId);
18755
- if (index < 0) {
18756
- this.config.log.warn({
18757
- chatId,
18758
- throughEntryId
18759
- }, "attempt completion ignored for untracked inbox entry");
18760
- return;
18761
- }
18762
- const committed = queue.splice(0, index + 1);
18763
- for (const tracked of committed) this.deduplicator.drop(tracked.dedupKey);
18764
- if (queue.length === 0) this.inFlightEntries.delete(chatId);
18765
- this.config.ackEntry(throughEntryId).catch((err) => {
18766
- this.config.log.warn({
18767
- chatId,
18768
- entryId: throughEntryId,
18769
- err
18770
- }, "ACK-through failed, continuing");
18771
- });
18772
- }
18773
- ackOldestTrackedEntry(chatId) {
18774
- const entryId = this.inFlightEntries.get(chatId)?.[0]?.entryId;
18775
- if (entryId !== void 0) this.ackThroughTrackedEntry(chatId, entryId);
18776
- }
18777
- clearInFlightForRecovery(chatId, reason) {
18778
- const queue = this.inFlightEntries.get(chatId);
18779
- if (!queue || queue.length === 0) return;
18780
- this.inFlightEntries.delete(chatId);
18781
- this.deduplicator.dropByPrefix(`${chatId}:`);
18782
- this.requiresInboxRecovery.add(chatId);
18783
- this.config.log.warn({
18784
- chatId,
18785
- reason,
18786
- entryIds: queue.map((entry) => entry.entryId)
18787
- }, "cleared local in-flight inbox entries; waiting for recovery redelivery");
18788
- }
18789
- ackConsumedInFlightForSuspend(chatId) {
18790
- const queue = this.inFlightEntries.get(chatId);
18791
- if (!queue || queue.length === 0) return;
18792
- let consumedPrefixCount = 0;
18793
- for (const tracked of queue) {
18794
- if (!tracked.consumed) break;
18795
- consumedPrefixCount++;
18796
- }
18797
- if (consumedPrefixCount === 0) return;
18798
- const lastConsumed = queue[consumedPrefixCount - 1];
18799
- if (lastConsumed) this.ackThroughTrackedEntry(chatId, lastConsumed.entryId);
18800
- }
18801
- markMessagesConsumed(chatId, messages) {
18802
- const queue = this.inFlightEntries.get(chatId);
18803
- if (!queue || queue.length === 0) return;
18804
- const batch = Array.isArray(messages) ? messages : [messages];
18805
- const consumedIds = /* @__PURE__ */ new Set();
18806
- for (const message of batch) if (message.chatId === chatId && message.inboxEntryId !== void 0) consumedIds.add(message.inboxEntryId);
18807
- if (consumedIds.size === 0) return;
18808
- for (const tracked of queue) if (consumedIds.has(tracked.entryId)) tracked.consumed = true;
18809
- }
18810
- markMessagesCompleted(chatId, messages) {
18811
- const batch = Array.isArray(messages) ? messages : [messages];
18812
- let throughEntryId;
18813
- for (const message of batch) {
18814
- if (message.chatId !== chatId) continue;
18815
- if (message.inboxEntryId !== void 0) throughEntryId = message.inboxEntryId;
18816
- }
18817
- if (throughEntryId === void 0) {
18818
- this.config.log.warn({ chatId }, "attempt completion ignored because no inboxEntryId was provided");
18819
- return;
18820
- }
18821
- this.ackThroughTrackedEntry(chatId, throughEntryId);
18822
- }
18823
- markMessagesRetryable(chatId, messages, reason) {
18824
- if (!(Array.isArray(messages) ? messages : [messages]).some((message) => message.chatId === chatId && message.inboxEntryId !== void 0 && (this.inFlightEntries.get(chatId) ?? []).some((entry) => entry.entryId === message.inboxEntryId))) return;
18825
- this.clearInFlightForRecovery(chatId, reason);
18826
- }
18827
- /**
18828
- * Drain every in-flight entry for this chat and ack them all. Used by
18829
- * the runtime on `session:terminate` (operator intent — every queued
18830
- * entry is doomed) and on permanent `handler.start` / `handler.resume`
18831
- * failure (re-handling on redelivery would re-hit the same permanent
18832
- * error, so acking avoids a loop). NOT to be called from handlers — the
18833
- * per-turn pairing path is `markMessagesCompleted(messageOrBatch)`.
18834
- */
18835
- drainAllInFlightEntries(chatId) {
18836
- const queue = this.inFlightEntries.get(chatId);
18837
- if (!queue || queue.length === 0) return;
18838
- let acceptedPrefixCount = 0;
18839
- for (const tracked of queue) {
18840
- if (!tracked.accepted) break;
18841
- acceptedPrefixCount++;
18842
- }
18843
- if (acceptedPrefixCount > 0) {
18844
- const lastAccepted = queue[acceptedPrefixCount - 1];
18845
- if (lastAccepted) this.ackThroughTrackedEntry(chatId, lastAccepted.entryId);
18846
- }
18847
- if (this.inFlightEntries.has(chatId)) this.clearInFlightForRecovery(chatId, "drain_unaccepted_remainder");
18848
- }
18849
19039
  buildSessionContext(chatId) {
18850
19040
  const sessionLog = this.config.log.child({ chatId });
18851
19041
  const log = (msg) => sessionLog.info(msg);
@@ -18886,28 +19076,22 @@ var SessionManager = class {
18886
19076
  sdk: this.config.sdk,
18887
19077
  log,
18888
19078
  chatId,
18889
- touch: () => {
19079
+ recordProviderActivity: () => {
18890
19080
  const entry = this.sessions.get(chatId);
18891
19081
  if (entry && entry.status === "active") entry.lastActivity = Date.now();
18892
19082
  },
18893
- setRuntimeState: (state) => {
18894
- this.setSessionRuntimeState(chatId, state);
18895
- },
18896
19083
  emitEvent: (event) => {
18897
19084
  this.config.onSessionEvent?.(chatId, event);
18898
19085
  },
18899
19086
  forwardResult,
18900
- markCompleted: () => {
18901
- this.ackOldestTrackedEntry(chatId);
18902
- },
18903
19087
  markMessagesConsumed: (messages) => {
18904
- this.markMessagesConsumed(chatId, messages);
19088
+ this.inboxDelivery.markConsumed(chatId, messages);
18905
19089
  },
18906
- markMessagesCompleted: (messages) => {
18907
- this.markMessagesCompleted(chatId, messages);
19090
+ finishTurn: (messages, outcome) => {
19091
+ return this.inboxDelivery.finishTurn(chatId, messages, outcome);
18908
19092
  },
18909
- markMessagesRetryable: (messages, reason) => {
18910
- this.markMessagesRetryable(chatId, messages, reason);
19093
+ retryTurn: (messages, reason) => {
19094
+ this.inboxDelivery.retryTurn(chatId, messages, reason);
18911
19095
  },
18912
19096
  buildAgentEnv: (parentEnv) => buildAgentEnv(parentEnv, envCtx),
18913
19097
  formatInboundContent: (message) => formatInboundContent(message, participants),
@@ -18925,14 +19109,24 @@ var SessionManager = class {
18925
19109
  return { agentHome: sessionRoot };
18926
19110
  }
18927
19111
  }
18928
- /** Update per-session runtime state and recompute aggregate. Only active sessions may update. */
18929
- setSessionRuntimeState(chatId, state) {
19112
+ projectSessionRuntime(chatId) {
18930
19113
  const session = this.sessions.get(chatId);
18931
- if (!session || session.status !== "active") return;
19114
+ const state = this.projectedRuntimeState(chatId, session ?? null);
19115
+ if (!state) {
19116
+ if (this.sessionRuntimeStates.delete(chatId)) this.recomputeRuntimeState();
19117
+ return;
19118
+ }
19119
+ if (this.sessionRuntimeStates.get(chatId) === state) return;
18932
19120
  this.sessionRuntimeStates.set(chatId, state);
18933
19121
  this.config.onSessionRuntimeChange?.(chatId, state);
18934
19122
  this.recomputeRuntimeState();
18935
19123
  }
19124
+ projectedRuntimeState(chatId, session) {
19125
+ if (!session) return null;
19126
+ if (session.status === "errored") return "error";
19127
+ if (session.status !== "active") return null;
19128
+ return this.inboxDelivery.hasUnsettledWork(chatId) ? "working" : "idle";
19129
+ }
18936
19130
  /**
18937
19131
  * Re-affirm working / blocked / error sessions so the server-side
18938
19132
  * freshness stamp doesn't lapse mid-turn. Reports only — does NOT
@@ -18944,9 +19138,9 @@ var SessionManager = class {
18944
19138
  reaffirmRuntimeStates() {
18945
19139
  if (!this.config.onSessionRuntimeChange) return;
18946
19140
  for (const [chatId, session] of this.sessions) {
18947
- if (session.status !== "active") continue;
19141
+ if (session.status !== "active" && session.status !== "errored") continue;
18948
19142
  const state = this.sessionRuntimeStates.get(chatId);
18949
- if (state === "working" || state === "blocked" || state === "error") this.config.onSessionRuntimeChange(chatId, state);
19143
+ if (state === "working" || state === "error") this.config.onSessionRuntimeChange(chatId, state);
18950
19144
  }
18951
19145
  }
18952
19146
  /**
@@ -18959,10 +19153,11 @@ var SessionManager = class {
18959
19153
  getSessionRuntimeStates() {
18960
19154
  const out = [];
18961
19155
  for (const [chatId, session] of this.sessions) {
18962
- if (session.status !== "active") continue;
19156
+ const runtimeState = this.projectedRuntimeState(chatId, session);
19157
+ if (!runtimeState) continue;
18963
19158
  out.push({
18964
19159
  chatId,
18965
- runtimeState: this.sessionRuntimeStates.get(chatId) ?? "idle"
19160
+ runtimeState
18966
19161
  });
18967
19162
  }
18968
19163
  return out;
@@ -19288,7 +19483,7 @@ var AgentSlot = class {
19288
19483
  * Ack happens INSIDE the SessionManager via the `ackEntry` callback we
19289
19484
  * pinned at construction time — `clientConnection.sendInboxAck`. Post
19290
19485
  * inflight-message-recovery the ack is deferred until the handler closes
19291
- * the turn via `ctx.markCompleted()`. Sending an additional ack here
19486
+ * the turn via `ctx.finishTurn(...)`. Sending an additional ack here
19292
19487
  * would double-ack: a WS frame the server cannot match against any
19293
19488
  * `delivered` row, which leaks the server's per-agent in-flight counter
19294
19489
  * and stalls push after `inboxMaxInFlightPerAgent` messages.
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { A as readWorkspaceManifest, B as printResults, Bt as SdkError, C as onboardCreate, D as computeWorkspaceStatus, E as promoteToWorkspace, H as getClientServiceStatus, I as checkClientConfig, J as startClientService, K as resolveCliInvocation, L as checkNodeVersion, O as discoverWorkspaceRoot, P as checkAgentConfigs, R as checkServerReachable, S as onboardCheck, St as status, T as migrateWorkspaceToW1, U as installClientService, W as isServiceSupported, X as uninstallClientService, Y as stopClientService, Z as ClientRuntime, at as ensureFreshAccessToken, b as promptMissingFields, ct as resolveAccessToken, it as AuthRefreshRateLimitedError, j as writeWorkspaceManifest, k as pickImmediateWorkspaceSources, lt as resolveServerUrl, nt as rotateClientIdWithBackup, ot as ensureFreshAdminToken, q as restartClientService, r as deriveHubUrlFromToken, rt as AuthRefreshFailedError, t as HubUrlDerivationError, tt as handleClientOrgMismatch, v as isInteractive, vt as blank, w as detectMigrationState, x as formatCheckReport, y as promptAddAgent, z as checkWebSocket, zt as FirstTreeHubSDK } from "./connect-token-B-RM1nMS.mjs";
1
+ import { A as readWorkspaceManifest, B as printResults, Bt as SdkError, C as onboardCreate, D as computeWorkspaceStatus, E as promoteToWorkspace, H as getClientServiceStatus, I as checkClientConfig, J as startClientService, K as resolveCliInvocation, L as checkNodeVersion, O as discoverWorkspaceRoot, P as checkAgentConfigs, R as checkServerReachable, S as onboardCheck, St as status, T as migrateWorkspaceToW1, U as installClientService, W as isServiceSupported, X as uninstallClientService, Y as stopClientService, Z as ClientRuntime, at as ensureFreshAccessToken, b as promptMissingFields, ct as resolveAccessToken, it as AuthRefreshRateLimitedError, j as writeWorkspaceManifest, k as pickImmediateWorkspaceSources, lt as resolveServerUrl, nt as rotateClientIdWithBackup, ot as ensureFreshAdminToken, q as restartClientService, r as deriveHubUrlFromToken, rt as AuthRefreshFailedError, t as HubUrlDerivationError, tt as handleClientOrgMismatch, v as isInteractive, vt as blank, w as detectMigrationState, x as formatCheckReport, y as promptAddAgent, z as checkWebSocket, zt as FirstTreeHubSDK } from "./connect-token-Cd0Z_mdb.mjs";
2
2
  export { AuthRefreshFailedError, AuthRefreshRateLimitedError, ClientRuntime, FirstTreeHubSDK, FirstTreeHubSDK as FirstTreeSDK, HubUrlDerivationError, SdkError, blank, checkAgentConfigs, checkClientConfig, checkNodeVersion, checkServerReachable, checkWebSocket, computeWorkspaceStatus, deriveHubUrlFromToken, detectMigrationState, discoverWorkspaceRoot, ensureFreshAccessToken, ensureFreshAdminToken, formatCheckReport, getClientServiceStatus, handleClientOrgMismatch, installClientService, isInteractive, isServiceSupported, migrateWorkspaceToW1, onboardCheck, onboardCreate, pickImmediateWorkspaceSources, printResults, promoteToWorkspace, promptAddAgent, promptMissingFields, readWorkspaceManifest, resolveAccessToken, resolveCliInvocation, resolveServerUrl, restartClientService, rotateClientIdWithBackup, startClientService, status, stopClientService, uninstallClientService, writeWorkspaceManifest };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "first-tree-staging",
3
- "version": "0.5.7-staging.366.1",
3
+ "version": "0.5.7-staging.367.1",
4
4
  "type": "module",
5
5
  "description": "First Tree — unified CLI for Context Tree onboarding, agent management, and team messaging. (Source-tree dev build; CI rewrites `name` and `bin` for prod / staging publishes — see docs/local-dev-isolation.md.)",
6
6
  "keywords": [