@kirkchinese/dsh-citeciter 0.3.2 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as canonicalCitationIdentity, c as citationSelectionClaimSchema, d as citeCiterSettingsSchema, f as renderCitationContext, i as TUTOR_SECTION_NAME, l as citeCiterRequestSchema, n as CITECITER_SETTINGS_NAMESPACE, o as citationDraftSchema, p as topicMetadataSchema, r as DEFAULT_CITECITER_SETTINGS, t as CITATION_CONTEXT_NAME } from "./topic-DByv84H6.js";
1
+ import { a as canonicalCitationIdentity, c as citationSelectionClaimSchema, d as citeCiterSettingsSchema, f as renderCitationContext, i as TUTOR_SECTION_NAME, l as citeCiterRequestSchema, n as CITECITER_SETTINGS_NAMESPACE, o as citationDraftSchema, p as topicMetadataSchema, r as DEFAULT_CITECITER_SETTINGS, t as CITATION_CONTEXT_NAME } from "./topic-BVNCaVbJ.js";
2
2
  import { Context, Service } from "@deepseek-ai/cordis";
3
3
  import { settingsNamespace } from "@deepseek-ai/dsh-settings";
4
4
  import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
@@ -9,7 +9,7 @@ import { isAbsolute, matchesGlob, relative, resolve } from "node:path";
9
9
  import AgentRegistry, { installModelSelection } from "@deepseek-ai/dsh-agent";
10
10
  import AgentLoop from "@deepseek-ai/dsh-agent-loop";
11
11
  import { dshHomePath } from "@deepseek-ai/dsh-home-paths";
12
- import { BlockAssembler, ReasoningEffortId, createUserMessage } from "@deepseek-ai/dsh-llm";
12
+ import { BlockAssembler, MessageId, ReasoningEffortId, createUserMessage, freezeMessage } from "@deepseek-ai/dsh-llm";
13
13
  import { effectiveSandboxMode, setSandboxMode } from "@deepseek-ai/dsh-sandbox-policy";
14
14
  import SessionStore, { SessionId, foldRequestHeader, snapshotJsonValue } from "@deepseek-ai/dsh-session";
15
15
  import JsonlSessionPersistence from "@deepseek-ai/dsh-session-persistence-jsonl";
@@ -14641,12 +14641,18 @@ function formatSourceSessionRead(source, options) {
14641
14641
  if (!Number.isSafeInteger(fromSeq) || fromSeq < 0) throw new Error("fromSeq must be a non-negative safe integer");
14642
14642
  if (options.throughSeq !== void 0 && (!Number.isSafeInteger(options.throughSeq) || options.throughSeq < fromSeq)) throw new Error("throughSeq must be a safe integer greater than or equal to fromSeq");
14643
14643
  if (!Number.isSafeInteger(options.maxBytes) || options.maxBytes < 2) throw new Error("maxBytes must be a safe integer of at least 2");
14644
- const availableThroughSeq = source.events.length === 0 ? null : source.events[source.events.length - 1]?.seq ?? null;
14644
+ let availableThroughSeq = null;
14645
+ for (const event of source.events) {
14646
+ if (options.throughSeq !== void 0 && event.seq > options.throughSeq) break;
14647
+ availableThroughSeq = event.seq;
14648
+ }
14645
14649
  const events = [];
14646
14650
  let bytesUsed = 2;
14647
14651
  let capturedThroughSeq = null;
14648
14652
  let truncated = false;
14649
- for (const event of source.events) {
14653
+ for (let index = 0; index < source.events.length; index += 1) {
14654
+ const event = source.events[index];
14655
+ if (event === void 0) continue;
14650
14656
  if (event.seq < fromSeq) continue;
14651
14657
  if (options.throughSeq !== void 0 && event.seq > options.throughSeq) break;
14652
14658
  const formatted = formatEvidenceEvent(event, options.includeReasoning);
@@ -14654,10 +14660,33 @@ function formatSourceSessionRead(source, options) {
14654
14660
  capturedThroughSeq = event.seq;
14655
14661
  continue;
14656
14662
  }
14657
- const eventBytes = Buffer.byteLength(JSON.stringify(formatted), "utf8") + (events.length === 0 ? 0 : 1);
14663
+ const serializedBytes = Buffer.byteLength(JSON.stringify(formatted), "utf8");
14664
+ const eventBytes = serializedBytes + (events.length === 0 ? 0 : 1);
14658
14665
  if (bytesUsed + eventBytes > options.maxBytes) {
14659
- truncated = true;
14660
- break;
14666
+ if (serializedBytes <= options.maxBytes - 2) {
14667
+ truncated = true;
14668
+ break;
14669
+ }
14670
+ const placeholder = evidence({
14671
+ type: event.type,
14672
+ seq: event.seq,
14673
+ oversized: true
14674
+ });
14675
+ const serializedPlaceholderBytes = Buffer.byteLength(JSON.stringify(placeholder), "utf8");
14676
+ const placeholderBytes = serializedPlaceholderBytes + (events.length === 0 ? 0 : 1);
14677
+ if (bytesUsed + placeholderBytes > options.maxBytes) {
14678
+ if (serializedPlaceholderBytes <= options.maxBytes - 2) {
14679
+ truncated = true;
14680
+ break;
14681
+ }
14682
+ capturedThroughSeq = event.seq;
14683
+ truncated = true;
14684
+ break;
14685
+ }
14686
+ events.push(placeholder);
14687
+ bytesUsed += placeholderBytes;
14688
+ capturedThroughSeq = event.seq;
14689
+ continue;
14661
14690
  }
14662
14691
  events.push(formatted);
14663
14692
  bytesUsed += eventBytes;
@@ -14694,6 +14723,10 @@ const TOPIC_TITLE_CONFIG = resolveSessionTitleLlmConfig({
14694
14723
  maxOutputTokens: 64,
14695
14724
  timeoutMs: 6e4
14696
14725
  });
14726
+ const CITECITER_SHUTTING_DOWN = "CiteCiter is shutting down";
14727
+ function citeCiterShuttingDownError() {
14728
+ return /* @__PURE__ */ new Error(CITECITER_SHUTTING_DOWN);
14729
+ }
14697
14730
  /** Decide both model visibility and execution access for one private Topic tool. */
14698
14731
  function citeCiterToolAvailable(name, allowSourceFiles) {
14699
14732
  return ALWAYS_AVAILABLE_TOOLS.has(name) || allowSourceFiles && SOURCE_FILE_TOOLS.has(name);
@@ -15049,6 +15082,54 @@ function topicMessages(log) {
15049
15082
  error
15050
15083
  };
15051
15084
  }
15085
+ /**
15086
+ * Return the first genuine Topic question after any Exact Fork seed.
15087
+ * @param log - private Topic Session contents.
15088
+ * @returns the first post-seed question, or `null` when it has not been committed.
15089
+ */
15090
+ function firstPostSeedUserQuestion(log) {
15091
+ for (const event of log.events.slice(log.header.seedLength ?? 0)) {
15092
+ if (event.type !== "user/message" || event.data.source.kind !== "user") continue;
15093
+ const text = textBlocks(event.data.content, "text");
15094
+ if (text !== "") return text;
15095
+ }
15096
+ for (const message of pendingPostSeedUserMessages(log)) {
15097
+ if (message.source.kind !== "user") continue;
15098
+ const text = textBlocks(message.content, "text");
15099
+ if (text !== "") return text;
15100
+ }
15101
+ return null;
15102
+ }
15103
+ function pendingPostSeedUserMessages(log) {
15104
+ const pending = {
15105
+ "next-turn": [],
15106
+ "next-step": []
15107
+ };
15108
+ for (const event of log.events.slice(log.header.seedLength ?? 0)) {
15109
+ if (event.type !== "agent/inbox/spliced") continue;
15110
+ pending[event.data.target].splice(event.data.start, event.data.removedCount ?? 0, ...event.data.inserted);
15111
+ }
15112
+ return [...pending["next-step"], ...pending["next-turn"]];
15113
+ }
15114
+ /**
15115
+ * Find a post-seed user question by its durable message identifier.
15116
+ * @param log - private Topic Session contents.
15117
+ * @param messageId - request identity stored as the user-message identity.
15118
+ * @returns the matching question, or `null` when the request is not committed.
15119
+ */
15120
+ function postSeedUserQuestionById(log, messageId) {
15121
+ const committed = committedPostSeedUserQuestionById(log, messageId);
15122
+ if (committed !== null) return committed;
15123
+ const pending = pendingPostSeedUserMessages(log).find((message) => message.source.kind === "user" && String(message.id) === messageId);
15124
+ return pending === void 0 ? null : textBlocks(pending.content, "text");
15125
+ }
15126
+ function committedPostSeedUserQuestionById(log, messageId) {
15127
+ for (const event of log.events.slice(log.header.seedLength ?? 0)) {
15128
+ if (event.type !== "user/message" || event.data.source.kind !== "user" || String(event.data.id) !== messageId) continue;
15129
+ return textBlocks(event.data.content, "text");
15130
+ }
15131
+ return null;
15132
+ }
15052
15133
  function titleSourceKind(value) {
15053
15134
  if (value === void 0) return null;
15054
15135
  return value.source.kind === "fallback" || value.source.kind === "provider" || value.source.kind === "user" ? value.source.kind : null;
@@ -15110,18 +15191,34 @@ function resolveTopicModeAndSeed(requested, source, anchorSeq) {
15110
15191
  function createSourceSessionId(request) {
15111
15192
  return "selectionClaim" in request ? request.selectionClaim.sourceSessionId : request.citation.sourceSessionId;
15112
15193
  }
15194
+ function identifiedQuestion(requestId, question) {
15195
+ return freezeMessage({
15196
+ id: MessageId(requestId),
15197
+ role: "user",
15198
+ content: [{
15199
+ type: "text",
15200
+ text: question
15201
+ }],
15202
+ source: { kind: "user" }
15203
+ });
15204
+ }
15113
15205
  /** One process-local private DSH tree with standard Session logs and Agent loop. */
15114
15206
  var TopicRuntime = class {
15115
15207
  host;
15116
15208
  settings;
15117
15209
  runtime = new Context();
15118
15210
  index = new TopicIndex();
15211
+ lifecycleAbort = new AbortController();
15119
15212
  fibers = [];
15120
15213
  handles = /* @__PURE__ */ new Map();
15121
15214
  selections = /* @__PURE__ */ new Map();
15122
15215
  opening = /* @__PURE__ */ new Map();
15216
+ requests = /* @__PURE__ */ new Set();
15217
+ cleanupFailures = [];
15123
15218
  pendingQuestions = /* @__PURE__ */ new Map();
15124
15219
  creations = /* @__PURE__ */ new Map();
15220
+ asks = /* @__PURE__ */ new Map();
15221
+ topicAdmissions = /* @__PURE__ */ new Map();
15125
15222
  modelChanges = /* @__PURE__ */ new Map();
15126
15223
  titleRefreshes = /* @__PURE__ */ new Map();
15127
15224
  titleRefreshAttempted = /* @__PURE__ */ new Set();
@@ -15150,66 +15247,74 @@ var TopicRuntime = class {
15150
15247
  return this.ready;
15151
15248
  }
15152
15249
  /** Execute one validated browser command against private Topics. */
15153
- async request(rawRequest) {
15250
+ async request(rawRequest, callerSignal) {
15154
15251
  const request = citeCiterRequestSchema.parse(rawRequest);
15155
15252
  await this.ready;
15156
- if (this.closed) throw new Error("CiteCiter is shutting down");
15253
+ const signal = AbortSignal.any([this.lifecycleAbort.signal, callerSignal]);
15254
+ this.assertOpen(signal);
15255
+ const operation = this.executeRequest(request, signal);
15256
+ this.requests.add(operation);
15257
+ operation.then(() => this.requests.delete(operation), () => this.requests.delete(operation));
15258
+ return operation;
15259
+ }
15260
+ async executeRequest(request, signal) {
15261
+ this.assertOpen(signal);
15157
15262
  switch (request.action) {
15158
15263
  case "create": return {
15159
15264
  kind: "topic",
15160
- topic: await this.createIdempotent(request)
15265
+ topic: await this.createIdempotent(request, signal)
15161
15266
  };
15162
15267
  case "list": return {
15163
15268
  kind: "topics",
15164
- topics: await this.list(request.sourceSessionId, request.includeArchived ?? false)
15269
+ topics: await this.list(request.sourceSessionId, request.includeArchived ?? false, signal)
15165
15270
  };
15166
15271
  case "get": return {
15167
15272
  kind: "topic",
15168
- topic: await this.snapshot(await this.index.loadBySessionId(request.topicSessionId))
15273
+ topic: await this.get(request.topicSessionId, signal)
15169
15274
  };
15170
15275
  case "ask": return {
15171
15276
  kind: "topic",
15172
- topic: await this.ask(request.topicSessionId, request.question)
15277
+ topic: await this.askIdempotent(request, signal)
15173
15278
  };
15174
15279
  case "stop": return {
15175
15280
  kind: "topic",
15176
- topic: await this.stop(request.topicSessionId)
15281
+ topic: await this.stop(request.topicSessionId, signal)
15177
15282
  };
15178
15283
  case "answer-question": return {
15179
15284
  kind: "topic",
15180
- topic: await this.answerQuestion(request)
15285
+ topic: await this.answerQuestion(request, signal)
15181
15286
  };
15182
15287
  case "cancel-question": return {
15183
15288
  kind: "topic",
15184
- topic: await this.cancelQuestion(request.topicSessionId, request.key)
15289
+ topic: await this.cancelQuestion(request.topicSessionId, request.key, signal)
15185
15290
  };
15186
15291
  case "rename": return {
15187
15292
  kind: "topic",
15188
- topic: await this.rename(request.topicSessionId, request.title)
15293
+ topic: await this.rename(request.topicSessionId, request.title, signal)
15189
15294
  };
15190
15295
  case "archive": return {
15191
15296
  kind: "topic",
15192
- topic: await this.archive(request.topicSessionId, request.archived)
15297
+ topic: await this.archive(request.topicSessionId, request.archived, signal)
15193
15298
  };
15194
15299
  case "delete": return {
15195
15300
  kind: "deleted",
15196
- sessionId: await this.delete(request.topicSessionId, request.confirmSessionId)
15301
+ sessionId: await this.delete(request.topicSessionId, request.confirmSessionId, signal)
15197
15302
  };
15198
15303
  case "models": return {
15199
15304
  kind: "models",
15200
- providers: await this.models()
15305
+ providers: await this.models(signal)
15201
15306
  };
15202
15307
  case "set-model-route": return {
15203
15308
  kind: "topic",
15204
- topic: await this.setModelRoute(request)
15309
+ topic: await this.setModelRoute(request, signal)
15205
15310
  };
15206
15311
  case "set-reasoning-effort": return {
15207
15312
  kind: "topic",
15208
- topic: await this.setReasoningEffort(request)
15313
+ topic: await this.setReasoningEffort(request, signal)
15209
15314
  };
15210
15315
  case "select-model": return {
15211
15316
  kind: "topic",
15212
- topic: await this.selectModel(request)
15317
+ topic: await this.selectModel(request, signal)
15213
15318
  };
15214
15319
  default: return request;
15215
15320
  }
@@ -15220,10 +15325,19 @@ var TopicRuntime = class {
15220
15325
  return this.disposal;
15221
15326
  }
15222
15327
  async disposeOwned() {
15223
- this.closed = true;
15328
+ this.beginClosing();
15224
15329
  await this.ready.catch(() => void 0);
15225
15330
  await this.releaseRuntime();
15226
15331
  }
15332
+ beginClosing() {
15333
+ if (this.closed) return;
15334
+ this.closed = true;
15335
+ this.lifecycleAbort.abort(citeCiterShuttingDownError());
15336
+ }
15337
+ assertOpen(signal) {
15338
+ if (this.closed) throw citeCiterShuttingDownError();
15339
+ signal?.throwIfAborted();
15340
+ }
15227
15341
  async start() {
15228
15342
  try {
15229
15343
  this.releaseLlm = this.runtime.provide("llm", this.host.llm);
@@ -15275,6 +15389,7 @@ var TopicRuntime = class {
15275
15389
  this.fibers.push(await this.runtime.plugin(TopicTitleProvider));
15276
15390
  this.fibers.push(await this.runtime.plugin(AgentLoop, { agents: [] }));
15277
15391
  } catch (error) {
15392
+ this.beginClosing();
15278
15393
  try {
15279
15394
  await this.releaseRuntime();
15280
15395
  } catch (cleanupError) {
@@ -15288,37 +15403,47 @@ var TopicRuntime = class {
15288
15403
  return this.releasing;
15289
15404
  }
15290
15405
  async releaseOwnedRuntime() {
15291
- await Promise.allSettled([...this.opening.values()]);
15292
- this.opening.clear();
15293
15406
  const failures = [];
15294
- for (const pending of this.pendingQuestions.values()) {
15295
- pending.signal?.removeEventListener("abort", pending.onAbort);
15296
- pending.reject(new UserQuestionError("CiteCiter is shutting down", "ASK_ABORTED"));
15297
- }
15298
- this.pendingQuestions.clear();
15299
15407
  try {
15300
15408
  this.releaseQuestionProvider?.();
15301
15409
  } catch (error) {
15302
15410
  failures.push(error);
15303
15411
  }
15304
15412
  this.releaseQuestionProvider = void 0;
15413
+ for (const pending of this.pendingQuestions.values()) {
15414
+ pending.signal?.removeEventListener("abort", pending.onAbort);
15415
+ pending.reject(new UserQuestionError(CITECITER_SHUTTING_DOWN, "ASK_ABORTED"));
15416
+ }
15417
+ this.pendingQuestions.clear();
15418
+ const handleDisposals = [];
15305
15419
  for (const handle of [...this.handles.values()]) try {
15306
- await handle.dispose();
15420
+ handleDisposals.push(handle.dispose().catch((error) => {
15421
+ failures.push(error);
15422
+ }));
15307
15423
  } catch (error) {
15308
15424
  failures.push(error);
15309
15425
  }
15310
15426
  this.handles.clear();
15427
+ await this.settleOwnedOperations();
15428
+ await Promise.all(handleDisposals);
15429
+ failures.push(...this.cleanupFailures.splice(0));
15311
15430
  for (const fiber of this.fibers.splice(0).reverse()) try {
15312
15431
  await fiber.dispose();
15313
15432
  } catch (error) {
15314
15433
  failures.push(error);
15315
15434
  }
15316
- await Promise.allSettled([...this.titleRefreshes.values()]);
15435
+ this.requests.clear();
15436
+ this.creations.clear();
15437
+ this.asks.clear();
15438
+ this.topicAdmissions.clear();
15439
+ this.modelChanges.clear();
15440
+ this.sourceAvailabilityChecks.clear();
15317
15441
  this.titleRefreshes.clear();
15442
+ this.opening.clear();
15318
15443
  for (const release of [
15319
15444
  this.releaseSandboxPolicy,
15320
- this.releaseFs,
15321
15445
  this.releaseSubprocess,
15446
+ this.releaseFs,
15322
15447
  this.releaseLlm
15323
15448
  ]) try {
15324
15449
  await release?.();
@@ -15331,9 +15456,26 @@ var TopicRuntime = class {
15331
15456
  this.releaseLlm = void 0;
15332
15457
  if (failures.length > 0) throw new AggregateError(failures, "CiteCiter Topic runtime cleanup failed");
15333
15458
  }
15334
- async create(request) {
15459
+ async settleOwnedOperations() {
15460
+ while (true) {
15461
+ const operations = /* @__PURE__ */ new Set([
15462
+ ...this.requests,
15463
+ ...[...this.creations.values()].map(({ result }) => result),
15464
+ ...[...this.asks.values()].map(({ result }) => result),
15465
+ ...this.topicAdmissions.values(),
15466
+ ...this.modelChanges.values(),
15467
+ ...this.sourceAvailabilityChecks.values(),
15468
+ ...this.titleRefreshes.values(),
15469
+ ...this.opening.values()
15470
+ ]);
15471
+ if (operations.size === 0) return;
15472
+ await Promise.allSettled(operations);
15473
+ }
15474
+ }
15475
+ async create(request, signal) {
15335
15476
  const sourceSessionId = createSourceSessionId(request);
15336
15477
  const source = await this.host.sessionQuery.readSession(SessionId(sourceSessionId));
15478
+ this.assertOpen(signal);
15337
15479
  this.sourceAvailability.set(sourceSessionId, true);
15338
15480
  const validated = "selectionClaim" in request ? resolveObserverCitation(source, request.selectionClaim) : validateObserverCitation(source, request.citation);
15339
15481
  const { topicId, directory } = await this.index.reserve(sourceSessionId);
@@ -15377,17 +15519,11 @@ var TopicRuntime = class {
15377
15519
  };
15378
15520
  let handle;
15379
15521
  try {
15380
- handle = await this.createHandle(metadata, mode.seed);
15522
+ handle = await this.createHandle(metadata, mode.seed, signal);
15381
15523
  await this.runtime.sessions.flush(handle.agent.session);
15524
+ this.assertOpen(signal);
15382
15525
  await this.index.save(metadata);
15383
- handle.agent.followup(createUserMessage({
15384
- content: [{
15385
- type: "text",
15386
- text: request.question
15387
- }],
15388
- source: { kind: "user" }
15389
- }));
15390
- await this.runtime.sessions.flush(handle.agent.session);
15526
+ await this.commitFollowup(handle, identifiedQuestion(request.requestId, request.question), signal);
15391
15527
  return this.snapshot(metadata);
15392
15528
  } catch (error) {
15393
15529
  try {
@@ -15405,16 +15541,66 @@ var TopicRuntime = class {
15405
15541
  throw error;
15406
15542
  }
15407
15543
  }
15408
- async createIdempotent(request) {
15544
+ /** Let a caller stop waiting without cancelling an accepted idempotent mutation. */
15545
+ waitForCaller(operation, signal) {
15546
+ if (signal === void 0) return operation;
15547
+ return new Promise((resolve, reject) => {
15548
+ const cleanup = () => signal.removeEventListener("abort", onAbort);
15549
+ const onAbort = () => {
15550
+ cleanup();
15551
+ reject(signal.reason);
15552
+ };
15553
+ signal.addEventListener("abort", onAbort, { once: true });
15554
+ if (signal.aborted) onAbort();
15555
+ operation.then((value) => {
15556
+ cleanup();
15557
+ resolve(value);
15558
+ }, (error) => {
15559
+ cleanup();
15560
+ reject(error);
15561
+ });
15562
+ });
15563
+ }
15564
+ createIdempotent(request, signal) {
15565
+ const key = `${createSourceSessionId(request)}\0${request.requestId}`;
15566
+ const pending = this.creations.get(key);
15567
+ const intent = JSON.stringify(request);
15568
+ if (pending !== void 0) {
15569
+ if (pending.intent !== intent) throw new Error("CiteCiter create requestId was reused for a different request");
15570
+ return this.waitForCaller(pending.result, signal);
15571
+ }
15572
+ const creation = Promise.resolve().then(() => {
15573
+ this.assertOpen(signal);
15574
+ return this.resumeOrCreate(request, signal);
15575
+ }).finally(() => this.creations.delete(key));
15576
+ this.creations.set(key, {
15577
+ intent,
15578
+ result: creation
15579
+ });
15580
+ return this.waitForCaller(creation, signal);
15581
+ }
15582
+ async resumeOrCreate(request, signal) {
15409
15583
  const committed = (await this.index.list(createSourceSessionId(request))).find((topic) => topic.createRequestId === request.requestId);
15410
- if (committed !== void 0) return this.snapshot(committed);
15411
- const pending = this.creations.get(request.requestId);
15412
- if (pending !== void 0) return pending;
15413
- const creation = this.create(request).finally(() => this.creations.delete(request.requestId));
15414
- this.creations.set(request.requestId, creation);
15415
- return creation;
15584
+ this.assertOpen(signal);
15585
+ if (committed !== void 0) return this.queueTopicAdmission(committed.sessionId, async () => {
15586
+ const log = await this.readLog(committed, signal);
15587
+ const identified = postSeedUserQuestionById(log, request.requestId);
15588
+ const existingQuestion = identified ?? firstPostSeedUserQuestion(log);
15589
+ if (existingQuestion !== null && existingQuestion !== request.question) throw new Error("CiteCiter create requestId was reused for a different question");
15590
+ if (existingQuestion === null || identified !== null && committedPostSeedUserQuestionById(log, request.requestId) === null) {
15591
+ const handle = await this.ensureHandle(committed, signal);
15592
+ handle.agent.inbox.remove(MessageId(request.requestId));
15593
+ await this.commitFollowup(handle, identifiedQuestion(request.requestId, request.question), signal);
15594
+ } else {
15595
+ const live = this.handles.get(committed.sessionId)?.agent.session;
15596
+ if (live !== void 0) await this.runtime.sessions.flush(live);
15597
+ }
15598
+ return this.snapshot(committed);
15599
+ }, signal);
15600
+ return this.create(request, signal);
15416
15601
  }
15417
- async createHandle(metadata, seed) {
15602
+ async createHandle(metadata, seed, signal) {
15603
+ this.assertOpen(signal);
15418
15604
  const handle = await this.runtime.agents.create({
15419
15605
  sessionId: SessionId(metadata.sessionId),
15420
15606
  ...metadata.mode === "exact-fork" ? {
@@ -15430,8 +15616,13 @@ var TopicRuntime = class {
15430
15616
  model: metadata.modelConfig.model,
15431
15617
  ...metadata.modelConfig.maxTokens === void 0 ? {} : { maxTokens: metadata.modelConfig.maxTokens }
15432
15618
  },
15433
- setup: (agentCtx) => this.setupAgent(agentCtx, metadata)
15619
+ setup: (agentCtx) => this.setupAgent(agentCtx, metadata),
15620
+ ...signal === void 0 ? {} : { signal }
15434
15621
  });
15622
+ if (this.closed || signal?.aborted === true) {
15623
+ await this.disposeLateHandle(handle);
15624
+ this.assertOpen(signal);
15625
+ }
15435
15626
  this.handles.set(metadata.sessionId, handle);
15436
15627
  return handle;
15437
15628
  }
@@ -15644,12 +15835,13 @@ var TopicRuntime = class {
15644
15835
  }],
15645
15836
  presentationMeta: (_args, value) => ({ capturedThroughSeq: value.capturedThroughSeq })
15646
15837
  },
15647
- execute: async (args) => {
15838
+ execute: async (args, exec) => {
15648
15839
  let source;
15649
15840
  let sourceAvailable = true;
15650
15841
  try {
15651
15842
  source = await this.host.sessionQuery.readSession(SessionId(metadata.sourceSessionId));
15652
15843
  } catch (error) {
15844
+ exec.signal.throwIfAborted();
15653
15845
  sourceAvailable = false;
15654
15846
  const agent = agentCtx.agent;
15655
15847
  if (metadata.mode !== "exact-fork" || agent === void 0 || agent.session.header.seedLength === void 0) {
@@ -15661,12 +15853,14 @@ var TopicRuntime = class {
15661
15853
  events: agent.session.events.slice(0, agent.session.header.seedLength)
15662
15854
  };
15663
15855
  }
15856
+ exec.signal.throwIfAborted();
15664
15857
  await this.rememberSourceAvailability(metadata, sourceAvailable);
15665
- const requestedThrough = args.throughSeq;
15666
- const throughSeq = metadata.forkThroughSeq === null ? requestedThrough : Math.min(requestedThrough ?? metadata.forkThroughSeq, metadata.forkThroughSeq);
15667
- const result = formatSourceSessionRead(source, {
15858
+ const result = formatSourceSessionRead(metadata.forkThroughSeq === null ? source : {
15859
+ ...source,
15860
+ events: source.events.filter((event) => event.seq <= metadata.forkThroughSeq)
15861
+ }, {
15668
15862
  ...args.fromSeq === void 0 ? {} : { fromSeq: args.fromSeq },
15669
- ...throughSeq === void 0 ? {} : { throughSeq },
15863
+ ...args.throughSeq === void 0 ? {} : { throughSeq: args.throughSeq },
15670
15864
  includeReasoning: this.settings().includeSourceReasoning,
15671
15865
  maxBytes: SOURCE_READ_MAX_BYTES
15672
15866
  });
@@ -15685,7 +15879,8 @@ var TopicRuntime = class {
15685
15879
  })
15686
15880
  });
15687
15881
  }
15688
- async ensureHandle(metadata) {
15882
+ async ensureHandle(metadata, signal) {
15883
+ this.assertOpen(signal);
15689
15884
  const existing = this.handles.get(metadata.sessionId);
15690
15885
  if (existing !== void 0) return existing;
15691
15886
  const pending = this.opening.get(metadata.sessionId);
@@ -15697,8 +15892,13 @@ var TopicRuntime = class {
15697
15892
  model: metadata.modelConfig.model,
15698
15893
  ...metadata.modelConfig.maxTokens === void 0 ? {} : { maxTokens: metadata.modelConfig.maxTokens }
15699
15894
  },
15700
- setup: (agentCtx) => this.setupAgent(agentCtx, metadata)
15701
- }).then((handle) => {
15895
+ setup: (agentCtx) => this.setupAgent(agentCtx, metadata),
15896
+ ...signal === void 0 ? {} : { signal }
15897
+ }).then(async (handle) => {
15898
+ if (this.closed || signal?.aborted === true) {
15899
+ await this.disposeLateHandle(handle);
15900
+ this.assertOpen(signal);
15901
+ }
15702
15902
  this.handles.set(metadata.sessionId, handle);
15703
15903
  return handle;
15704
15904
  }).finally(() => {
@@ -15707,15 +15907,86 @@ var TopicRuntime = class {
15707
15907
  this.opening.set(metadata.sessionId, opening);
15708
15908
  return opening;
15709
15909
  }
15710
- async ask(sessionId, question) {
15910
+ async disposeLateHandle(handle) {
15911
+ try {
15912
+ await handle.dispose();
15913
+ } catch (error) {
15914
+ this.cleanupFailures.push(error);
15915
+ throw error;
15916
+ }
15917
+ }
15918
+ /** Resolve only after the accepted question is present in the durable model-input log. */
15919
+ async commitFollowup(handle, message, admissionSignal) {
15920
+ this.assertOpen(admissionSignal);
15921
+ const signal = this.lifecycleAbort.signal;
15922
+ await new Promise((resolveCommitted, rejectCommitted) => {
15923
+ let claimedTurn;
15924
+ let settled = false;
15925
+ let disposeClaim = () => {};
15926
+ let disposeDiscard = () => {};
15927
+ let disposeEvent = () => {};
15928
+ const onAbort = () => finish(signal?.reason ?? citeCiterShuttingDownError());
15929
+ const finish = (error) => {
15930
+ if (settled) return;
15931
+ settled = true;
15932
+ signal?.removeEventListener("abort", onAbort);
15933
+ disposeEvent();
15934
+ disposeDiscard();
15935
+ disposeClaim();
15936
+ if (error === void 0) resolveCommitted();
15937
+ else rejectCommitted(error);
15938
+ };
15939
+ disposeClaim = handle.agent.ctx.on("agent/inbox/claimed", ({ message: claimed, turn }) => {
15940
+ if (claimed.id === message.id) claimedTurn = turn;
15941
+ });
15942
+ disposeDiscard = handle.agent.ctx.on("agent/inbox/discarded", ({ message: discarded }) => {
15943
+ if (discarded.id === message.id) finish(/* @__PURE__ */ new Error("CiteCiter question was discarded before it became model input"));
15944
+ });
15945
+ disposeEvent = handle.agent.ctx.on("session/event", (session, event) => {
15946
+ if (session !== handle.agent.session) return;
15947
+ if (event.type === "user/message" && event.data.source.kind === "user" && event.data.id === message.id) {
15948
+ finish();
15949
+ return;
15950
+ }
15951
+ if (event.type === "turn/end" && event.data.turn === claimedTurn) finish(/* @__PURE__ */ new Error("CiteCiter question was not committed before its turn ended"));
15952
+ });
15953
+ signal?.addEventListener("abort", onAbort, { once: true });
15954
+ if (signal?.aborted === true) {
15955
+ onAbort();
15956
+ return;
15957
+ }
15958
+ try {
15959
+ handle.agent.followup(message);
15960
+ } catch (error) {
15961
+ finish(error);
15962
+ }
15963
+ });
15964
+ await this.runtime.sessions.flush(handle.agent.session);
15965
+ }
15966
+ async ask(sessionId, question, requestId, signal) {
15711
15967
  const metadata = await this.index.loadBySessionId(sessionId);
15712
- (await this.ensureHandle(metadata)).agent.followup(createUserMessage({
15968
+ this.assertOpen(signal);
15969
+ if (requestId !== void 0) {
15970
+ const log = await this.readLog(metadata, signal);
15971
+ const existingQuestion = postSeedUserQuestionById(log, requestId);
15972
+ if (existingQuestion !== null) {
15973
+ if (existingQuestion !== question) throw new Error("CiteCiter ask requestId was reused for a different question");
15974
+ if (committedPostSeedUserQuestionById(log, requestId) !== null) {
15975
+ const live = this.handles.get(sessionId)?.agent.session;
15976
+ if (live !== void 0) await this.runtime.sessions.flush(live);
15977
+ return this.snapshot(metadata);
15978
+ }
15979
+ }
15980
+ }
15981
+ const handle = await this.ensureHandle(metadata, signal);
15982
+ if (requestId !== void 0) handle.agent.inbox.remove(MessageId(requestId));
15983
+ await this.commitFollowup(handle, requestId === void 0 ? createUserMessage({
15713
15984
  content: [{
15714
15985
  type: "text",
15715
15986
  text: question
15716
15987
  }],
15717
15988
  source: { kind: "user" }
15718
- }));
15989
+ }) : identifiedQuestion(requestId, question), signal);
15719
15990
  const updated = {
15720
15991
  ...metadata,
15721
15992
  updatedAt: Date.now()
@@ -15723,7 +15994,38 @@ var TopicRuntime = class {
15723
15994
  await this.index.save(updated);
15724
15995
  return this.snapshot(updated);
15725
15996
  }
15997
+ async askIdempotent(request, signal) {
15998
+ if (request.requestId === void 0) return this.queueAsk(request, signal);
15999
+ const key = `${request.topicSessionId}\0${request.requestId}`;
16000
+ const existing = this.asks.get(key);
16001
+ if (existing !== void 0) {
16002
+ if (existing.question !== request.question) throw new Error("CiteCiter ask requestId was reused for a different question");
16003
+ return this.waitForCaller(existing.result, signal);
16004
+ }
16005
+ const result = this.queueAsk(request, signal).finally(() => this.asks.delete(key));
16006
+ this.asks.set(key, {
16007
+ question: request.question,
16008
+ result
16009
+ });
16010
+ return this.waitForCaller(result, signal);
16011
+ }
16012
+ queueAsk(request, signal) {
16013
+ return this.queueTopicAdmission(request.topicSessionId, () => this.ask(request.topicSessionId, request.question, request.requestId, signal), signal);
16014
+ }
16015
+ queueTopicAdmission(sessionId, operation, signal) {
16016
+ const result = (this.topicAdmissions.get(sessionId) ?? Promise.resolve()).then(() => {
16017
+ this.assertOpen(signal);
16018
+ return operation();
16019
+ });
16020
+ const settled = result.then(() => void 0, () => void 0);
16021
+ this.topicAdmissions.set(sessionId, settled);
16022
+ settled.then(() => {
16023
+ if (this.topicAdmissions.get(sessionId) === settled) this.topicAdmissions.delete(sessionId);
16024
+ });
16025
+ return result;
16026
+ }
15726
16027
  askUser(request) {
16028
+ if (this.closed) throw new UserQuestionError(CITECITER_SHUTTING_DOWN, "ASK_ABORTED");
15727
16029
  const sessionId = request.agent === void 0 ? void 0 : String(request.agent.session.header.id);
15728
16030
  if (sessionId === void 0 || !this.handles.has(sessionId)) throw new UserQuestionError("CiteCiter cannot identify the asking Topic", "CALLER_NOT_LIVE");
15729
16031
  if (this.pendingQuestions.has(sessionId)) throw new UserQuestionError("this Topic already has a pending question", "DUPLICATE_QUESTION");
@@ -15756,31 +16058,36 @@ var TopicRuntime = class {
15756
16058
  if (request.signal?.aborted === true) onAbort();
15757
16059
  });
15758
16060
  }
15759
- async answerQuestion(request) {
16061
+ async answerQuestion(request, signal) {
15760
16062
  const metadata = await this.index.loadBySessionId(request.topicSessionId);
16063
+ this.assertOpen(signal);
15761
16064
  const pending = this.pendingQuestions.get(request.topicSessionId);
15762
16065
  if (pending === void 0 || pending.key !== request.key) throw new Error("这个提问已结束或已被替换");
15763
16066
  pending.resolve(validatedQuestionAnswer(pending.questions, request.answer));
15764
16067
  return this.snapshot(metadata);
15765
16068
  }
15766
- async cancelQuestion(sessionId, key) {
16069
+ async cancelQuestion(sessionId, key, signal) {
15767
16070
  const metadata = await this.index.loadBySessionId(sessionId);
16071
+ this.assertOpen(signal);
15768
16072
  const pending = this.pendingQuestions.get(sessionId);
15769
16073
  if (pending === void 0 || pending.key !== key) throw new Error("这个提问已结束或已被替换");
15770
16074
  pending.reject(new UserQuestionError("the user cancelled ask_user_question", "ASK_CANCELLED"));
15771
16075
  return this.snapshot(metadata);
15772
16076
  }
15773
- async stop(sessionId) {
16077
+ async stop(sessionId, signal) {
15774
16078
  const metadata = await this.index.loadBySessionId(sessionId);
16079
+ this.assertOpen(signal);
15775
16080
  const agent = this.handles.get(sessionId)?.agent;
15776
16081
  agent?.cancel({ kind: "user" });
15777
16082
  await agent?.whenIdle();
15778
16083
  if (agent !== void 0) await this.runtime.sessions.flush(agent.session);
15779
16084
  return this.snapshot(metadata);
15780
16085
  }
15781
- async rename(sessionId, title) {
16086
+ async rename(sessionId, title, signal) {
15782
16087
  const metadata = await this.index.loadBySessionId(sessionId);
15783
- const handle = await this.ensureHandle(metadata);
16088
+ this.assertOpen(signal);
16089
+ const handle = await this.ensureHandle(metadata, signal);
16090
+ this.assertOpen(signal);
15784
16091
  const renamed = this.runtime.sessionTitle.rename(handle.agent.session, title);
15785
16092
  await this.runtime.sessions.flush(handle.agent.session);
15786
16093
  const updated = {
@@ -15793,25 +16100,29 @@ var TopicRuntime = class {
15793
16100
  await this.index.save(updated);
15794
16101
  return this.snapshot(updated);
15795
16102
  }
15796
- async archive(sessionId, archived) {
16103
+ async archive(sessionId, archived, signal) {
16104
+ const metadata = await this.index.loadBySessionId(sessionId);
16105
+ this.assertOpen(signal);
15797
16106
  const updated = {
15798
- ...await this.index.loadBySessionId(sessionId),
16107
+ ...metadata,
15799
16108
  archivedAt: archived ? Date.now() : null,
15800
16109
  updatedAt: Date.now()
15801
16110
  };
15802
16111
  await this.index.save(updated);
15803
16112
  return this.snapshot(updated);
15804
16113
  }
15805
- async delete(sessionId, confirmSessionId) {
16114
+ async delete(sessionId, confirmSessionId, signal) {
15806
16115
  if (sessionId !== confirmSessionId) throw new Error("Topic deletion confirmation does not match the target Session");
15807
16116
  const metadata = await this.index.loadBySessionId(sessionId);
16117
+ this.assertOpen(signal);
15808
16118
  const pending = this.opening.get(sessionId);
15809
16119
  const handle = this.handles.get(sessionId) ?? (pending === void 0 ? void 0 : await pending);
16120
+ this.assertOpen(signal);
15810
16121
  if (handle !== void 0) {
15811
16122
  await handle.dispose();
15812
16123
  this.handles.delete(sessionId);
15813
16124
  }
15814
- const inspection = await this.runtime.sessionPersistence.inspect(SessionId(sessionId));
16125
+ const inspection = await this.runtime.sessionPersistence.inspect(SessionId(sessionId), signal);
15815
16126
  await this.removeSessionArtifact(inspection.meta);
15816
16127
  await this.index.remove(metadata);
15817
16128
  return sessionId;
@@ -15826,20 +16137,25 @@ var TopicRuntime = class {
15826
16137
  }) !== void 0) await unlink(artifact.path);
15827
16138
  await rmdirIfEmpty(resolve(artifact.path, ".."));
15828
16139
  }
15829
- enqueueModelChange(sessionId, apply) {
16140
+ enqueueModelChange(sessionId, apply, signal) {
15830
16141
  const previous = this.modelChanges.get(sessionId);
15831
16142
  let change;
15832
- change = (previous === void 0 ? Promise.resolve() : previous.catch(() => void 0)).then(apply).finally(() => {
16143
+ change = (previous === void 0 ? Promise.resolve() : previous.catch(() => void 0)).then(() => {
16144
+ this.assertOpen(signal);
16145
+ return apply();
16146
+ }).finally(() => {
15833
16147
  if (this.modelChanges.get(sessionId) === change) this.modelChanges.delete(sessionId);
15834
16148
  });
15835
16149
  this.modelChanges.set(sessionId, change);
15836
16150
  return change;
15837
16151
  }
15838
- setModelRoute(request) {
16152
+ setModelRoute(request, signal) {
15839
16153
  return this.enqueueModelChange(request.topicSessionId, async () => {
15840
16154
  const metadata = await this.index.loadBySessionId(request.topicSessionId);
15841
- await this.host.llm.resolveModelInfo(request.provider, request.model);
15842
- await this.ensureHandle(metadata);
16155
+ this.assertOpen(signal);
16156
+ await this.host.llm.resolveModelInfo(request.provider, request.model, signal);
16157
+ await this.ensureHandle(metadata, signal);
16158
+ this.assertOpen(signal);
15843
16159
  const selection = this.selections.get(metadata.sessionId);
15844
16160
  if (selection === void 0) throw new Error("Topic model selector is unavailable");
15845
16161
  const modelConfig = {
@@ -15859,14 +16175,16 @@ var TopicRuntime = class {
15859
16175
  model: request.model
15860
16176
  };
15861
16177
  return this.snapshot(updated);
15862
- });
16178
+ }, signal);
15863
16179
  }
15864
- setReasoningEffort(request) {
16180
+ setReasoningEffort(request, signal) {
15865
16181
  return this.enqueueModelChange(request.topicSessionId, async () => {
15866
16182
  const metadata = await this.index.loadBySessionId(request.topicSessionId);
15867
- const model = await this.host.llm.resolveModelInfo(metadata.modelConfig.provider, metadata.modelConfig.model);
16183
+ this.assertOpen(signal);
16184
+ const model = await this.host.llm.resolveModelInfo(metadata.modelConfig.provider, metadata.modelConfig.model, signal);
15868
16185
  if (request.reasoningEffort !== null && model.reasoning?.efforts.some((effort) => String(effort.id) === request.reasoningEffort) !== true) throw new Error(`模型不支持思考强度 ${request.reasoningEffort}`);
15869
- await this.ensureHandle(metadata);
16186
+ await this.ensureHandle(metadata, signal);
16187
+ this.assertOpen(signal);
15870
16188
  const selection = this.selections.get(metadata.sessionId);
15871
16189
  if (selection === void 0) throw new Error("Topic model selector is unavailable");
15872
16190
  const modelConfig = { ...metadata.modelConfig };
@@ -15884,16 +16202,18 @@ var TopicRuntime = class {
15884
16202
  ...request.reasoningEffort === null ? {} : { reasoningEffort: ReasoningEffortId(request.reasoningEffort) }
15885
16203
  };
15886
16204
  return this.snapshot(updated);
15887
- });
16205
+ }, signal);
15888
16206
  }
15889
- selectModel(request) {
15890
- return this.enqueueModelChange(request.topicSessionId, () => this.applyModelSelection(request));
16207
+ selectModel(request, signal) {
16208
+ return this.enqueueModelChange(request.topicSessionId, () => this.applyModelSelection(request, signal), signal);
15891
16209
  }
15892
- async applyModelSelection(request) {
16210
+ async applyModelSelection(request, signal) {
15893
16211
  const metadata = await this.index.loadBySessionId(request.topicSessionId);
15894
- const model = await this.host.llm.resolveModelInfo(request.provider, request.model);
16212
+ this.assertOpen(signal);
16213
+ const model = await this.host.llm.resolveModelInfo(request.provider, request.model, signal);
15895
16214
  if (request.reasoningEffort !== null && model.reasoning?.efforts.some((effort) => String(effort.id) === request.reasoningEffort) !== true) throw new Error(`模型不支持思考强度 ${request.reasoningEffort}`);
15896
- await this.ensureHandle(metadata);
16215
+ await this.ensureHandle(metadata, signal);
16216
+ this.assertOpen(signal);
15897
16217
  const selection = this.selections.get(metadata.sessionId);
15898
16218
  if (selection === void 0) throw new Error("Topic model selector is unavailable");
15899
16219
  const previousModelConfig = { ...metadata.modelConfig };
@@ -15916,13 +16236,16 @@ var TopicRuntime = class {
15916
16236
  };
15917
16237
  return this.snapshot(updated);
15918
16238
  }
15919
- async models() {
16239
+ async models(signal) {
15920
16240
  const providers = [];
15921
16241
  for (const provider of this.host.llm.listProviders()) {
16242
+ this.assertOpen(signal);
15922
16243
  let catalog;
15923
16244
  try {
15924
16245
  catalog = await this.host.llm.listModels(provider.id);
16246
+ this.assertOpen(signal);
15925
16247
  } catch (error) {
16248
+ signal?.throwIfAborted();
15926
16249
  this.host.logger.warn(`CiteCiter could not list models for ${provider.id}`, error);
15927
16250
  catalog = [];
15928
16251
  }
@@ -15930,8 +16253,9 @@ var TopicRuntime = class {
15930
16253
  for (const model of catalog) {
15931
16254
  let resolved;
15932
16255
  try {
15933
- resolved = await this.host.llm.resolveModelInfo(provider.id, model.id);
16256
+ resolved = await this.host.llm.resolveModelInfo(provider.id, model.id, signal);
15934
16257
  } catch (error) {
16258
+ signal?.throwIfAborted();
15935
16259
  this.host.logger.warn(`CiteCiter could not resolve ${provider.id}/${model.id}`, error);
15936
16260
  }
15937
16261
  models.push({
@@ -15952,21 +16276,22 @@ var TopicRuntime = class {
15952
16276
  }
15953
16277
  return providers;
15954
16278
  }
15955
- async list(sourceSessionId, includeArchived) {
16279
+ async list(sourceSessionId, includeArchived, signal) {
15956
16280
  const metadata = await this.index.list(sourceSessionId);
15957
- return (await Promise.all(metadata.filter((topic) => includeArchived ? topic.archivedAt !== null : topic.archivedAt === null).map((topic) => this.summary(topic)))).sort((left, right) => right.updatedAt - left.updatedAt);
16281
+ this.assertOpen(signal);
16282
+ return (await Promise.all(metadata.filter((topic) => includeArchived ? topic.archivedAt !== null : topic.archivedAt === null).map((topic) => this.summary(topic, signal)))).sort((left, right) => right.updatedAt - left.updatedAt);
15958
16283
  }
15959
- async summary(metadata) {
16284
+ async summary(metadata, signal) {
15960
16285
  let current = metadata;
15961
16286
  if (cachedTopicTitle(current) === null && !this.titleHydrated.has(current.sessionId)) {
16287
+ const log = await this.readLog(current, signal);
15962
16288
  this.titleHydrated.add(current.sessionId);
15963
- const log = await this.readLog(current);
15964
16289
  const title = foldTopicTitle(current, log.events);
15965
16290
  if (title !== void 0) current = await this.patchMetadata(current, {
15966
16291
  cachedTitle: title.title,
15967
16292
  cachedTitleSource: titleSourceKind(title),
15968
16293
  cachedTitleEventSeq: title.eventSeq
15969
- });
16294
+ }, signal);
15970
16295
  }
15971
16296
  return this.summaryFromMetadata(current);
15972
16297
  }
@@ -15989,20 +16314,27 @@ var TopicRuntime = class {
15989
16314
  modelConfig: metadata.modelConfig
15990
16315
  };
15991
16316
  }
15992
- async readLog(metadata) {
16317
+ async get(sessionId, signal) {
16318
+ const metadata = await this.index.loadBySessionId(sessionId);
16319
+ this.assertOpen(signal);
16320
+ return this.snapshot(metadata, signal);
16321
+ }
16322
+ async readLog(metadata, signal) {
16323
+ if (signal !== void 0) this.assertOpen(signal);
15993
16324
  const live = this.handles.get(metadata.sessionId)?.agent.session;
15994
16325
  if (live !== void 0) return {
15995
16326
  header: live.header,
15996
16327
  events: live.events
15997
16328
  };
15998
- const inspection = await this.runtime.sessionPersistence.inspect(SessionId(metadata.sessionId));
16329
+ const inspection = await this.runtime.sessionPersistence.inspect(SessionId(metadata.sessionId), signal);
16330
+ if (signal !== void 0) this.assertOpen(signal);
15999
16331
  return {
16000
16332
  header: inspection.meta,
16001
16333
  events: inspection.events
16002
16334
  };
16003
16335
  }
16004
16336
  scheduleSourceAvailabilityCheck(metadata) {
16005
- if (this.sourceAvailability.has(metadata.sourceSessionId) || this.sourceAvailabilityChecks.has(metadata.sourceSessionId)) return;
16337
+ if (this.closed || this.sourceAvailability.has(metadata.sourceSessionId) || this.sourceAvailabilityChecks.has(metadata.sourceSessionId)) return;
16006
16338
  const check = (async () => {
16007
16339
  let available = true;
16008
16340
  try {
@@ -16026,10 +16358,10 @@ var TopicRuntime = class {
16026
16358
  const latest = await this.index.loadBySessionId(metadata.sessionId);
16027
16359
  if (latest.sourceAvailable !== available) await this.patchMetadata(latest, { sourceAvailable: available });
16028
16360
  }
16029
- async snapshot(metadata) {
16361
+ async snapshot(metadata, signal) {
16030
16362
  let current = metadata;
16031
16363
  this.scheduleSourceAvailabilityCheck(current);
16032
- const log = await this.readLog(current);
16364
+ const log = await this.readLog(current, signal);
16033
16365
  const title = foldTopicTitle(current, log.events);
16034
16366
  const latest = log.events.at(-1)?.time ?? metadata.updatedAt;
16035
16367
  const observedThroughSeq = latestObservedSeq(log.events);
@@ -16042,7 +16374,7 @@ var TopicRuntime = class {
16042
16374
  cachedTitleSource,
16043
16375
  cachedTitleEventSeq: title.eventSeq
16044
16376
  }
16045
- });
16377
+ }, signal);
16046
16378
  if (title === void 0) this.scheduleExactTitleRefresh(current, log);
16047
16379
  const pending = this.pendingQuestions.get(current.sessionId);
16048
16380
  return {
@@ -16060,8 +16392,9 @@ var TopicRuntime = class {
16060
16392
  }
16061
16393
  };
16062
16394
  }
16063
- async patchMetadata(metadata, patch) {
16395
+ async patchMetadata(metadata, patch, signal) {
16064
16396
  const latest = await this.index.loadBySessionId(metadata.sessionId);
16397
+ if (signal !== void 0) this.assertOpen(signal);
16065
16398
  const updated = topicMetadataSchema.parse({
16066
16399
  ...latest,
16067
16400
  ...patch
@@ -16070,20 +16403,22 @@ var TopicRuntime = class {
16070
16403
  return updated;
16071
16404
  }
16072
16405
  scheduleExactTitleRefresh(metadata, log) {
16073
- if (metadata.mode !== "exact-fork" || this.titleRefreshAttempted.has(metadata.sessionId) || this.handles.get(metadata.sessionId)?.agent.status === "running") return;
16406
+ if (this.closed || metadata.mode !== "exact-fork" || this.titleRefreshAttempted.has(metadata.sessionId) || this.handles.get(metadata.sessionId)?.agent.status === "running") return;
16074
16407
  const postSeed = log.events.slice(log.header.seedLength ?? 0);
16075
16408
  if (!postSeed.some((event) => event.type === "request/header") || !postSeed.some((event) => event.type === "assistant/message")) return;
16076
16409
  const handle = this.handles.get(metadata.sessionId);
16077
16410
  if (handle === void 0) return;
16078
16411
  this.titleRefreshAttempted.add(metadata.sessionId);
16079
- const refresh = this.runtime.sessionTitle.refresh(handle.agent.session).then(async (title) => {
16412
+ const refresh = this.runtime.sessionTitle.refresh(handle.agent.session, this.lifecycleAbort.signal).then(async (title) => {
16413
+ this.assertOpen(this.lifecycleAbort.signal);
16080
16414
  await this.runtime.sessions.flush(handle.agent.session);
16415
+ this.assertOpen(this.lifecycleAbort.signal);
16081
16416
  if (title === void 0 || title.eventSeq <= (metadata.forkThroughSeq ?? -1)) return;
16082
16417
  await this.patchMetadata(metadata, {
16083
16418
  cachedTitle: title.title,
16084
16419
  cachedTitleSource: titleSourceKind(title),
16085
16420
  cachedTitleEventSeq: title.eventSeq
16086
- });
16421
+ }, this.lifecycleAbort.signal);
16087
16422
  }).catch((error) => {
16088
16423
  if (!this.closed) this.host.logger.warn(`CiteCiter could not title Topic ${metadata.sessionId}`, error);
16089
16424
  }).finally(() => {
@@ -16200,8 +16535,8 @@ let CiteCiterHost = (() => {
16200
16535
  await this.topics.initialize();
16201
16536
  }
16202
16537
  /** Validate and execute one strict Topic command. */
16203
- async request(rawRequest) {
16204
- return this.topics.request(citeCiterRequestSchema.parse(rawRequest));
16538
+ async request(rawRequest, signal) {
16539
+ return this.topics.request(citeCiterRequestSchema.parse(rawRequest), signal);
16205
16540
  }
16206
16541
  };
16207
16542
  })();