@kirkchinese/dsh-citeciter 0.4.0 → 0.4.2

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";
@@ -14369,7 +14369,8 @@ function compactMapped(units) {
14369
14369
  });
14370
14370
  else compact[compact.length - 1] = {
14371
14371
  ...previous,
14372
- endOffset: unit.endOffset
14372
+ endOffset: unit.endOffset,
14373
+ ...previous.synthetic === true || unit.synthetic === true ? { synthetic: true } : {}
14373
14374
  };
14374
14375
  }
14375
14376
  return compact;
@@ -14384,7 +14385,8 @@ function joinMapped(parts, separator) {
14384
14385
  for (const text of separator) joined.push({
14385
14386
  text,
14386
14387
  startOffset: before,
14387
- endOffset: after
14388
+ endOffset: after,
14389
+ synthetic: true
14388
14390
  });
14389
14391
  }
14390
14392
  joined.push(...part);
@@ -14442,6 +14444,47 @@ function candidatesFromProjection(markdown, projection, needle) {
14442
14444
  }
14443
14445
  return candidates;
14444
14446
  }
14447
+ function candidatesIgnoringSyntheticWhitespace(markdown, projection, needle) {
14448
+ const rendered = visibleText(projection);
14449
+ const visibleOffsets = [0];
14450
+ for (const unit of projection) visibleOffsets.push(visibleOffsets.at(-1) + unit.text.length);
14451
+ const candidates = [];
14452
+ const seen = /* @__PURE__ */ new Set();
14453
+ for (let start = 0; start < projection.length; start++) {
14454
+ const first = projection[start];
14455
+ if (first.synthetic === true) continue;
14456
+ let unitIndex = start;
14457
+ let needleOffset = 0;
14458
+ let lastIndex = -1;
14459
+ while (needleOffset < needle.length && unitIndex < projection.length) {
14460
+ const unit = projection[unitIndex];
14461
+ if (unit.synthetic === true) {
14462
+ while (projection[unitIndex]?.synthetic === true) unitIndex++;
14463
+ needleOffset += /^\s+/u.exec(needle.slice(needleOffset))?.[0].length ?? 0;
14464
+ continue;
14465
+ }
14466
+ if (!needle.startsWith(unit.text, needleOffset)) break;
14467
+ needleOffset += unit.text.length;
14468
+ lastIndex = unitIndex;
14469
+ unitIndex++;
14470
+ }
14471
+ if (needleOffset !== needle.length || lastIndex < start) continue;
14472
+ const last = projection[lastIndex];
14473
+ const key = `${first.startOffset}:${last.endOffset}`;
14474
+ if (seen.has(key)) continue;
14475
+ seen.add(key);
14476
+ const visibleStart = visibleOffsets[start];
14477
+ const visibleEnd = visibleOffsets[lastIndex + 1];
14478
+ candidates.push({
14479
+ startOffset: first.startOffset,
14480
+ endOffset: last.endOffset,
14481
+ sourceText: markdown.slice(first.startOffset, last.endOffset),
14482
+ displayPrefix: rendered.slice(Math.max(0, visibleStart - 240), visibleStart),
14483
+ displaySuffix: rendered.slice(visibleEnd, visibleEnd + 240)
14484
+ });
14485
+ }
14486
+ return candidates;
14487
+ }
14445
14488
  /**
14446
14489
  * Locate every GFM source range that renders as one browser-visible selection.
14447
14490
  *
@@ -14458,7 +14501,11 @@ function markdownSourceCandidates(markdown, displayText) {
14458
14501
  if (needle === "") return [];
14459
14502
  const exact = candidatesFromProjection(markdown, projection, needle);
14460
14503
  if (exact.length > 0) return exact;
14461
- return candidatesFromProjection(markdown, compactMapped(projection), needle.replace(/\s+/gu, " "));
14504
+ const compactProjection = compactMapped(projection);
14505
+ const compactNeedle = needle.replace(/\s+/gu, " ");
14506
+ const compact = candidatesFromProjection(markdown, compactProjection, compactNeedle);
14507
+ if (compact.length > 0) return compact;
14508
+ return candidatesIgnoringSyntheticWhitespace(markdown, compactProjection, compactNeedle);
14462
14509
  }
14463
14510
  //#endregion
14464
14511
  //#region lib/types/citation-mapping.js
@@ -14641,12 +14688,18 @@ function formatSourceSessionRead(source, options) {
14641
14688
  if (!Number.isSafeInteger(fromSeq) || fromSeq < 0) throw new Error("fromSeq must be a non-negative safe integer");
14642
14689
  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
14690
  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;
14691
+ let availableThroughSeq = null;
14692
+ for (const event of source.events) {
14693
+ if (options.throughSeq !== void 0 && event.seq > options.throughSeq) break;
14694
+ availableThroughSeq = event.seq;
14695
+ }
14645
14696
  const events = [];
14646
14697
  let bytesUsed = 2;
14647
14698
  let capturedThroughSeq = null;
14648
14699
  let truncated = false;
14649
- for (const event of source.events) {
14700
+ for (let index = 0; index < source.events.length; index += 1) {
14701
+ const event = source.events[index];
14702
+ if (event === void 0) continue;
14650
14703
  if (event.seq < fromSeq) continue;
14651
14704
  if (options.throughSeq !== void 0 && event.seq > options.throughSeq) break;
14652
14705
  const formatted = formatEvidenceEvent(event, options.includeReasoning);
@@ -14654,10 +14707,33 @@ function formatSourceSessionRead(source, options) {
14654
14707
  capturedThroughSeq = event.seq;
14655
14708
  continue;
14656
14709
  }
14657
- const eventBytes = Buffer.byteLength(JSON.stringify(formatted), "utf8") + (events.length === 0 ? 0 : 1);
14710
+ const serializedBytes = Buffer.byteLength(JSON.stringify(formatted), "utf8");
14711
+ const eventBytes = serializedBytes + (events.length === 0 ? 0 : 1);
14658
14712
  if (bytesUsed + eventBytes > options.maxBytes) {
14659
- truncated = true;
14660
- break;
14713
+ if (serializedBytes <= options.maxBytes - 2) {
14714
+ truncated = true;
14715
+ break;
14716
+ }
14717
+ const placeholder = evidence({
14718
+ type: event.type,
14719
+ seq: event.seq,
14720
+ oversized: true
14721
+ });
14722
+ const serializedPlaceholderBytes = Buffer.byteLength(JSON.stringify(placeholder), "utf8");
14723
+ const placeholderBytes = serializedPlaceholderBytes + (events.length === 0 ? 0 : 1);
14724
+ if (bytesUsed + placeholderBytes > options.maxBytes) {
14725
+ if (serializedPlaceholderBytes <= options.maxBytes - 2) {
14726
+ truncated = true;
14727
+ break;
14728
+ }
14729
+ capturedThroughSeq = event.seq;
14730
+ truncated = true;
14731
+ break;
14732
+ }
14733
+ events.push(placeholder);
14734
+ bytesUsed += placeholderBytes;
14735
+ capturedThroughSeq = event.seq;
14736
+ continue;
14661
14737
  }
14662
14738
  events.push(formatted);
14663
14739
  bytesUsed += eventBytes;
@@ -14694,6 +14770,10 @@ const TOPIC_TITLE_CONFIG = resolveSessionTitleLlmConfig({
14694
14770
  maxOutputTokens: 64,
14695
14771
  timeoutMs: 6e4
14696
14772
  });
14773
+ const CITECITER_SHUTTING_DOWN = "CiteCiter is shutting down";
14774
+ function citeCiterShuttingDownError() {
14775
+ return /* @__PURE__ */ new Error(CITECITER_SHUTTING_DOWN);
14776
+ }
14697
14777
  /** Decide both model visibility and execution access for one private Topic tool. */
14698
14778
  function citeCiterToolAvailable(name, allowSourceFiles) {
14699
14779
  return ALWAYS_AVAILABLE_TOOLS.has(name) || allowSourceFiles && SOURCE_FILE_TOOLS.has(name);
@@ -15049,6 +15129,54 @@ function topicMessages(log) {
15049
15129
  error
15050
15130
  };
15051
15131
  }
15132
+ /**
15133
+ * Return the first genuine Topic question after any Exact Fork seed.
15134
+ * @param log - private Topic Session contents.
15135
+ * @returns the first post-seed question, or `null` when it has not been committed.
15136
+ */
15137
+ function firstPostSeedUserQuestion(log) {
15138
+ for (const event of log.events.slice(log.header.seedLength ?? 0)) {
15139
+ if (event.type !== "user/message" || event.data.source.kind !== "user") continue;
15140
+ const text = textBlocks(event.data.content, "text");
15141
+ if (text !== "") return text;
15142
+ }
15143
+ for (const message of pendingPostSeedUserMessages(log)) {
15144
+ if (message.source.kind !== "user") continue;
15145
+ const text = textBlocks(message.content, "text");
15146
+ if (text !== "") return text;
15147
+ }
15148
+ return null;
15149
+ }
15150
+ function pendingPostSeedUserMessages(log) {
15151
+ const pending = {
15152
+ "next-turn": [],
15153
+ "next-step": []
15154
+ };
15155
+ for (const event of log.events.slice(log.header.seedLength ?? 0)) {
15156
+ if (event.type !== "agent/inbox/spliced") continue;
15157
+ pending[event.data.target].splice(event.data.start, event.data.removedCount ?? 0, ...event.data.inserted);
15158
+ }
15159
+ return [...pending["next-step"], ...pending["next-turn"]];
15160
+ }
15161
+ /**
15162
+ * Find a post-seed user question by its durable message identifier.
15163
+ * @param log - private Topic Session contents.
15164
+ * @param messageId - request identity stored as the user-message identity.
15165
+ * @returns the matching question, or `null` when the request is not committed.
15166
+ */
15167
+ function postSeedUserQuestionById(log, messageId) {
15168
+ const committed = committedPostSeedUserQuestionById(log, messageId);
15169
+ if (committed !== null) return committed;
15170
+ const pending = pendingPostSeedUserMessages(log).find((message) => message.source.kind === "user" && String(message.id) === messageId);
15171
+ return pending === void 0 ? null : textBlocks(pending.content, "text");
15172
+ }
15173
+ function committedPostSeedUserQuestionById(log, messageId) {
15174
+ for (const event of log.events.slice(log.header.seedLength ?? 0)) {
15175
+ if (event.type !== "user/message" || event.data.source.kind !== "user" || String(event.data.id) !== messageId) continue;
15176
+ return textBlocks(event.data.content, "text");
15177
+ }
15178
+ return null;
15179
+ }
15052
15180
  function titleSourceKind(value) {
15053
15181
  if (value === void 0) return null;
15054
15182
  return value.source.kind === "fallback" || value.source.kind === "provider" || value.source.kind === "user" ? value.source.kind : null;
@@ -15110,18 +15238,34 @@ function resolveTopicModeAndSeed(requested, source, anchorSeq) {
15110
15238
  function createSourceSessionId(request) {
15111
15239
  return "selectionClaim" in request ? request.selectionClaim.sourceSessionId : request.citation.sourceSessionId;
15112
15240
  }
15241
+ function identifiedQuestion(requestId, question) {
15242
+ return freezeMessage({
15243
+ id: MessageId(requestId),
15244
+ role: "user",
15245
+ content: [{
15246
+ type: "text",
15247
+ text: question
15248
+ }],
15249
+ source: { kind: "user" }
15250
+ });
15251
+ }
15113
15252
  /** One process-local private DSH tree with standard Session logs and Agent loop. */
15114
15253
  var TopicRuntime = class {
15115
15254
  host;
15116
15255
  settings;
15117
15256
  runtime = new Context();
15118
15257
  index = new TopicIndex();
15258
+ lifecycleAbort = new AbortController();
15119
15259
  fibers = [];
15120
15260
  handles = /* @__PURE__ */ new Map();
15121
15261
  selections = /* @__PURE__ */ new Map();
15122
15262
  opening = /* @__PURE__ */ new Map();
15263
+ requests = /* @__PURE__ */ new Set();
15264
+ cleanupFailures = [];
15123
15265
  pendingQuestions = /* @__PURE__ */ new Map();
15124
15266
  creations = /* @__PURE__ */ new Map();
15267
+ asks = /* @__PURE__ */ new Map();
15268
+ topicAdmissions = /* @__PURE__ */ new Map();
15125
15269
  modelChanges = /* @__PURE__ */ new Map();
15126
15270
  titleRefreshes = /* @__PURE__ */ new Map();
15127
15271
  titleRefreshAttempted = /* @__PURE__ */ new Set();
@@ -15150,66 +15294,74 @@ var TopicRuntime = class {
15150
15294
  return this.ready;
15151
15295
  }
15152
15296
  /** Execute one validated browser command against private Topics. */
15153
- async request(rawRequest) {
15297
+ async request(rawRequest, callerSignal) {
15154
15298
  const request = citeCiterRequestSchema.parse(rawRequest);
15155
15299
  await this.ready;
15156
- if (this.closed) throw new Error("CiteCiter is shutting down");
15300
+ const signal = AbortSignal.any([this.lifecycleAbort.signal, callerSignal]);
15301
+ this.assertOpen(signal);
15302
+ const operation = this.executeRequest(request, signal);
15303
+ this.requests.add(operation);
15304
+ operation.then(() => this.requests.delete(operation), () => this.requests.delete(operation));
15305
+ return operation;
15306
+ }
15307
+ async executeRequest(request, signal) {
15308
+ this.assertOpen(signal);
15157
15309
  switch (request.action) {
15158
15310
  case "create": return {
15159
15311
  kind: "topic",
15160
- topic: await this.createIdempotent(request)
15312
+ topic: await this.createIdempotent(request, signal)
15161
15313
  };
15162
15314
  case "list": return {
15163
15315
  kind: "topics",
15164
- topics: await this.list(request.sourceSessionId, request.includeArchived ?? false)
15316
+ topics: await this.list(request.sourceSessionId, request.includeArchived ?? false, signal)
15165
15317
  };
15166
15318
  case "get": return {
15167
15319
  kind: "topic",
15168
- topic: await this.snapshot(await this.index.loadBySessionId(request.topicSessionId))
15320
+ topic: await this.get(request.topicSessionId, signal)
15169
15321
  };
15170
15322
  case "ask": return {
15171
15323
  kind: "topic",
15172
- topic: await this.ask(request.topicSessionId, request.question)
15324
+ topic: await this.askIdempotent(request, signal)
15173
15325
  };
15174
15326
  case "stop": return {
15175
15327
  kind: "topic",
15176
- topic: await this.stop(request.topicSessionId)
15328
+ topic: await this.stop(request.topicSessionId, signal)
15177
15329
  };
15178
15330
  case "answer-question": return {
15179
15331
  kind: "topic",
15180
- topic: await this.answerQuestion(request)
15332
+ topic: await this.answerQuestion(request, signal)
15181
15333
  };
15182
15334
  case "cancel-question": return {
15183
15335
  kind: "topic",
15184
- topic: await this.cancelQuestion(request.topicSessionId, request.key)
15336
+ topic: await this.cancelQuestion(request.topicSessionId, request.key, signal)
15185
15337
  };
15186
15338
  case "rename": return {
15187
15339
  kind: "topic",
15188
- topic: await this.rename(request.topicSessionId, request.title)
15340
+ topic: await this.rename(request.topicSessionId, request.title, signal)
15189
15341
  };
15190
15342
  case "archive": return {
15191
15343
  kind: "topic",
15192
- topic: await this.archive(request.topicSessionId, request.archived)
15344
+ topic: await this.archive(request.topicSessionId, request.archived, signal)
15193
15345
  };
15194
15346
  case "delete": return {
15195
15347
  kind: "deleted",
15196
- sessionId: await this.delete(request.topicSessionId, request.confirmSessionId)
15348
+ sessionId: await this.delete(request.topicSessionId, request.confirmSessionId, signal)
15197
15349
  };
15198
15350
  case "models": return {
15199
15351
  kind: "models",
15200
- providers: await this.models()
15352
+ providers: await this.models(signal)
15201
15353
  };
15202
15354
  case "set-model-route": return {
15203
15355
  kind: "topic",
15204
- topic: await this.setModelRoute(request)
15356
+ topic: await this.setModelRoute(request, signal)
15205
15357
  };
15206
15358
  case "set-reasoning-effort": return {
15207
15359
  kind: "topic",
15208
- topic: await this.setReasoningEffort(request)
15360
+ topic: await this.setReasoningEffort(request, signal)
15209
15361
  };
15210
15362
  case "select-model": return {
15211
15363
  kind: "topic",
15212
- topic: await this.selectModel(request)
15364
+ topic: await this.selectModel(request, signal)
15213
15365
  };
15214
15366
  default: return request;
15215
15367
  }
@@ -15220,10 +15372,19 @@ var TopicRuntime = class {
15220
15372
  return this.disposal;
15221
15373
  }
15222
15374
  async disposeOwned() {
15223
- this.closed = true;
15375
+ this.beginClosing();
15224
15376
  await this.ready.catch(() => void 0);
15225
15377
  await this.releaseRuntime();
15226
15378
  }
15379
+ beginClosing() {
15380
+ if (this.closed) return;
15381
+ this.closed = true;
15382
+ this.lifecycleAbort.abort(citeCiterShuttingDownError());
15383
+ }
15384
+ assertOpen(signal) {
15385
+ if (this.closed) throw citeCiterShuttingDownError();
15386
+ signal?.throwIfAborted();
15387
+ }
15227
15388
  async start() {
15228
15389
  try {
15229
15390
  this.releaseLlm = this.runtime.provide("llm", this.host.llm);
@@ -15275,6 +15436,7 @@ var TopicRuntime = class {
15275
15436
  this.fibers.push(await this.runtime.plugin(TopicTitleProvider));
15276
15437
  this.fibers.push(await this.runtime.plugin(AgentLoop, { agents: [] }));
15277
15438
  } catch (error) {
15439
+ this.beginClosing();
15278
15440
  try {
15279
15441
  await this.releaseRuntime();
15280
15442
  } catch (cleanupError) {
@@ -15288,37 +15450,47 @@ var TopicRuntime = class {
15288
15450
  return this.releasing;
15289
15451
  }
15290
15452
  async releaseOwnedRuntime() {
15291
- await Promise.allSettled([...this.opening.values()]);
15292
- this.opening.clear();
15293
15453
  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
15454
  try {
15300
15455
  this.releaseQuestionProvider?.();
15301
15456
  } catch (error) {
15302
15457
  failures.push(error);
15303
15458
  }
15304
15459
  this.releaseQuestionProvider = void 0;
15460
+ for (const pending of this.pendingQuestions.values()) {
15461
+ pending.signal?.removeEventListener("abort", pending.onAbort);
15462
+ pending.reject(new UserQuestionError(CITECITER_SHUTTING_DOWN, "ASK_ABORTED"));
15463
+ }
15464
+ this.pendingQuestions.clear();
15465
+ const handleDisposals = [];
15305
15466
  for (const handle of [...this.handles.values()]) try {
15306
- await handle.dispose();
15467
+ handleDisposals.push(handle.dispose().catch((error) => {
15468
+ failures.push(error);
15469
+ }));
15307
15470
  } catch (error) {
15308
15471
  failures.push(error);
15309
15472
  }
15310
15473
  this.handles.clear();
15474
+ await this.settleOwnedOperations();
15475
+ await Promise.all(handleDisposals);
15476
+ failures.push(...this.cleanupFailures.splice(0));
15311
15477
  for (const fiber of this.fibers.splice(0).reverse()) try {
15312
15478
  await fiber.dispose();
15313
15479
  } catch (error) {
15314
15480
  failures.push(error);
15315
15481
  }
15316
- await Promise.allSettled([...this.titleRefreshes.values()]);
15482
+ this.requests.clear();
15483
+ this.creations.clear();
15484
+ this.asks.clear();
15485
+ this.topicAdmissions.clear();
15486
+ this.modelChanges.clear();
15487
+ this.sourceAvailabilityChecks.clear();
15317
15488
  this.titleRefreshes.clear();
15489
+ this.opening.clear();
15318
15490
  for (const release of [
15319
15491
  this.releaseSandboxPolicy,
15320
- this.releaseFs,
15321
15492
  this.releaseSubprocess,
15493
+ this.releaseFs,
15322
15494
  this.releaseLlm
15323
15495
  ]) try {
15324
15496
  await release?.();
@@ -15331,9 +15503,26 @@ var TopicRuntime = class {
15331
15503
  this.releaseLlm = void 0;
15332
15504
  if (failures.length > 0) throw new AggregateError(failures, "CiteCiter Topic runtime cleanup failed");
15333
15505
  }
15334
- async create(request) {
15506
+ async settleOwnedOperations() {
15507
+ while (true) {
15508
+ const operations = /* @__PURE__ */ new Set([
15509
+ ...this.requests,
15510
+ ...[...this.creations.values()].map(({ result }) => result),
15511
+ ...[...this.asks.values()].map(({ result }) => result),
15512
+ ...this.topicAdmissions.values(),
15513
+ ...this.modelChanges.values(),
15514
+ ...this.sourceAvailabilityChecks.values(),
15515
+ ...this.titleRefreshes.values(),
15516
+ ...this.opening.values()
15517
+ ]);
15518
+ if (operations.size === 0) return;
15519
+ await Promise.allSettled(operations);
15520
+ }
15521
+ }
15522
+ async create(request, signal) {
15335
15523
  const sourceSessionId = createSourceSessionId(request);
15336
15524
  const source = await this.host.sessionQuery.readSession(SessionId(sourceSessionId));
15525
+ this.assertOpen(signal);
15337
15526
  this.sourceAvailability.set(sourceSessionId, true);
15338
15527
  const validated = "selectionClaim" in request ? resolveObserverCitation(source, request.selectionClaim) : validateObserverCitation(source, request.citation);
15339
15528
  const { topicId, directory } = await this.index.reserve(sourceSessionId);
@@ -15377,17 +15566,11 @@ var TopicRuntime = class {
15377
15566
  };
15378
15567
  let handle;
15379
15568
  try {
15380
- handle = await this.createHandle(metadata, mode.seed);
15569
+ handle = await this.createHandle(metadata, mode.seed, signal);
15381
15570
  await this.runtime.sessions.flush(handle.agent.session);
15571
+ this.assertOpen(signal);
15382
15572
  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);
15573
+ await this.commitFollowup(handle, identifiedQuestion(request.requestId, request.question), signal);
15391
15574
  return this.snapshot(metadata);
15392
15575
  } catch (error) {
15393
15576
  try {
@@ -15405,16 +15588,66 @@ var TopicRuntime = class {
15405
15588
  throw error;
15406
15589
  }
15407
15590
  }
15408
- async createIdempotent(request) {
15591
+ /** Let a caller stop waiting without cancelling an accepted idempotent mutation. */
15592
+ waitForCaller(operation, signal) {
15593
+ if (signal === void 0) return operation;
15594
+ return new Promise((resolve, reject) => {
15595
+ const cleanup = () => signal.removeEventListener("abort", onAbort);
15596
+ const onAbort = () => {
15597
+ cleanup();
15598
+ reject(signal.reason);
15599
+ };
15600
+ signal.addEventListener("abort", onAbort, { once: true });
15601
+ if (signal.aborted) onAbort();
15602
+ operation.then((value) => {
15603
+ cleanup();
15604
+ resolve(value);
15605
+ }, (error) => {
15606
+ cleanup();
15607
+ reject(error);
15608
+ });
15609
+ });
15610
+ }
15611
+ createIdempotent(request, signal) {
15612
+ const key = `${createSourceSessionId(request)}\0${request.requestId}`;
15613
+ const pending = this.creations.get(key);
15614
+ const intent = JSON.stringify(request);
15615
+ if (pending !== void 0) {
15616
+ if (pending.intent !== intent) throw new Error("CiteCiter create requestId was reused for a different request");
15617
+ return this.waitForCaller(pending.result, signal);
15618
+ }
15619
+ const creation = Promise.resolve().then(() => {
15620
+ this.assertOpen(signal);
15621
+ return this.resumeOrCreate(request, signal);
15622
+ }).finally(() => this.creations.delete(key));
15623
+ this.creations.set(key, {
15624
+ intent,
15625
+ result: creation
15626
+ });
15627
+ return this.waitForCaller(creation, signal);
15628
+ }
15629
+ async resumeOrCreate(request, signal) {
15409
15630
  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;
15631
+ this.assertOpen(signal);
15632
+ if (committed !== void 0) return this.queueTopicAdmission(committed.sessionId, async () => {
15633
+ const log = await this.readLog(committed, signal);
15634
+ const identified = postSeedUserQuestionById(log, request.requestId);
15635
+ const existingQuestion = identified ?? firstPostSeedUserQuestion(log);
15636
+ if (existingQuestion !== null && existingQuestion !== request.question) throw new Error("CiteCiter create requestId was reused for a different question");
15637
+ if (existingQuestion === null || identified !== null && committedPostSeedUserQuestionById(log, request.requestId) === null) {
15638
+ const handle = await this.ensureHandle(committed, signal);
15639
+ handle.agent.inbox.remove(MessageId(request.requestId));
15640
+ await this.commitFollowup(handle, identifiedQuestion(request.requestId, request.question), signal);
15641
+ } else {
15642
+ const live = this.handles.get(committed.sessionId)?.agent.session;
15643
+ if (live !== void 0) await this.runtime.sessions.flush(live);
15644
+ }
15645
+ return this.snapshot(committed);
15646
+ }, signal);
15647
+ return this.create(request, signal);
15416
15648
  }
15417
- async createHandle(metadata, seed) {
15649
+ async createHandle(metadata, seed, signal) {
15650
+ this.assertOpen(signal);
15418
15651
  const handle = await this.runtime.agents.create({
15419
15652
  sessionId: SessionId(metadata.sessionId),
15420
15653
  ...metadata.mode === "exact-fork" ? {
@@ -15430,8 +15663,13 @@ var TopicRuntime = class {
15430
15663
  model: metadata.modelConfig.model,
15431
15664
  ...metadata.modelConfig.maxTokens === void 0 ? {} : { maxTokens: metadata.modelConfig.maxTokens }
15432
15665
  },
15433
- setup: (agentCtx) => this.setupAgent(agentCtx, metadata)
15666
+ setup: (agentCtx) => this.setupAgent(agentCtx, metadata),
15667
+ ...signal === void 0 ? {} : { signal }
15434
15668
  });
15669
+ if (this.closed || signal?.aborted === true) {
15670
+ await this.disposeLateHandle(handle);
15671
+ this.assertOpen(signal);
15672
+ }
15435
15673
  this.handles.set(metadata.sessionId, handle);
15436
15674
  return handle;
15437
15675
  }
@@ -15644,12 +15882,13 @@ var TopicRuntime = class {
15644
15882
  }],
15645
15883
  presentationMeta: (_args, value) => ({ capturedThroughSeq: value.capturedThroughSeq })
15646
15884
  },
15647
- execute: async (args) => {
15885
+ execute: async (args, exec) => {
15648
15886
  let source;
15649
15887
  let sourceAvailable = true;
15650
15888
  try {
15651
15889
  source = await this.host.sessionQuery.readSession(SessionId(metadata.sourceSessionId));
15652
15890
  } catch (error) {
15891
+ exec.signal.throwIfAborted();
15653
15892
  sourceAvailable = false;
15654
15893
  const agent = agentCtx.agent;
15655
15894
  if (metadata.mode !== "exact-fork" || agent === void 0 || agent.session.header.seedLength === void 0) {
@@ -15661,12 +15900,14 @@ var TopicRuntime = class {
15661
15900
  events: agent.session.events.slice(0, agent.session.header.seedLength)
15662
15901
  };
15663
15902
  }
15903
+ exec.signal.throwIfAborted();
15664
15904
  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, {
15905
+ const result = formatSourceSessionRead(metadata.forkThroughSeq === null ? source : {
15906
+ ...source,
15907
+ events: source.events.filter((event) => event.seq <= metadata.forkThroughSeq)
15908
+ }, {
15668
15909
  ...args.fromSeq === void 0 ? {} : { fromSeq: args.fromSeq },
15669
- ...throughSeq === void 0 ? {} : { throughSeq },
15910
+ ...args.throughSeq === void 0 ? {} : { throughSeq: args.throughSeq },
15670
15911
  includeReasoning: this.settings().includeSourceReasoning,
15671
15912
  maxBytes: SOURCE_READ_MAX_BYTES
15672
15913
  });
@@ -15685,7 +15926,8 @@ var TopicRuntime = class {
15685
15926
  })
15686
15927
  });
15687
15928
  }
15688
- async ensureHandle(metadata) {
15929
+ async ensureHandle(metadata, signal) {
15930
+ this.assertOpen(signal);
15689
15931
  const existing = this.handles.get(metadata.sessionId);
15690
15932
  if (existing !== void 0) return existing;
15691
15933
  const pending = this.opening.get(metadata.sessionId);
@@ -15697,8 +15939,13 @@ var TopicRuntime = class {
15697
15939
  model: metadata.modelConfig.model,
15698
15940
  ...metadata.modelConfig.maxTokens === void 0 ? {} : { maxTokens: metadata.modelConfig.maxTokens }
15699
15941
  },
15700
- setup: (agentCtx) => this.setupAgent(agentCtx, metadata)
15701
- }).then((handle) => {
15942
+ setup: (agentCtx) => this.setupAgent(agentCtx, metadata),
15943
+ ...signal === void 0 ? {} : { signal }
15944
+ }).then(async (handle) => {
15945
+ if (this.closed || signal?.aborted === true) {
15946
+ await this.disposeLateHandle(handle);
15947
+ this.assertOpen(signal);
15948
+ }
15702
15949
  this.handles.set(metadata.sessionId, handle);
15703
15950
  return handle;
15704
15951
  }).finally(() => {
@@ -15707,15 +15954,86 @@ var TopicRuntime = class {
15707
15954
  this.opening.set(metadata.sessionId, opening);
15708
15955
  return opening;
15709
15956
  }
15710
- async ask(sessionId, question) {
15957
+ async disposeLateHandle(handle) {
15958
+ try {
15959
+ await handle.dispose();
15960
+ } catch (error) {
15961
+ this.cleanupFailures.push(error);
15962
+ throw error;
15963
+ }
15964
+ }
15965
+ /** Resolve only after the accepted question is present in the durable model-input log. */
15966
+ async commitFollowup(handle, message, admissionSignal) {
15967
+ this.assertOpen(admissionSignal);
15968
+ const signal = this.lifecycleAbort.signal;
15969
+ await new Promise((resolveCommitted, rejectCommitted) => {
15970
+ let claimedTurn;
15971
+ let settled = false;
15972
+ let disposeClaim = () => {};
15973
+ let disposeDiscard = () => {};
15974
+ let disposeEvent = () => {};
15975
+ const onAbort = () => finish(signal?.reason ?? citeCiterShuttingDownError());
15976
+ const finish = (error) => {
15977
+ if (settled) return;
15978
+ settled = true;
15979
+ signal?.removeEventListener("abort", onAbort);
15980
+ disposeEvent();
15981
+ disposeDiscard();
15982
+ disposeClaim();
15983
+ if (error === void 0) resolveCommitted();
15984
+ else rejectCommitted(error);
15985
+ };
15986
+ disposeClaim = handle.agent.ctx.on("agent/inbox/claimed", ({ message: claimed, turn }) => {
15987
+ if (claimed.id === message.id) claimedTurn = turn;
15988
+ });
15989
+ disposeDiscard = handle.agent.ctx.on("agent/inbox/discarded", ({ message: discarded }) => {
15990
+ if (discarded.id === message.id) finish(/* @__PURE__ */ new Error("CiteCiter question was discarded before it became model input"));
15991
+ });
15992
+ disposeEvent = handle.agent.ctx.on("session/event", (session, event) => {
15993
+ if (session !== handle.agent.session) return;
15994
+ if (event.type === "user/message" && event.data.source.kind === "user" && event.data.id === message.id) {
15995
+ finish();
15996
+ return;
15997
+ }
15998
+ if (event.type === "turn/end" && event.data.turn === claimedTurn) finish(/* @__PURE__ */ new Error("CiteCiter question was not committed before its turn ended"));
15999
+ });
16000
+ signal?.addEventListener("abort", onAbort, { once: true });
16001
+ if (signal?.aborted === true) {
16002
+ onAbort();
16003
+ return;
16004
+ }
16005
+ try {
16006
+ handle.agent.followup(message);
16007
+ } catch (error) {
16008
+ finish(error);
16009
+ }
16010
+ });
16011
+ await this.runtime.sessions.flush(handle.agent.session);
16012
+ }
16013
+ async ask(sessionId, question, requestId, signal) {
15711
16014
  const metadata = await this.index.loadBySessionId(sessionId);
15712
- (await this.ensureHandle(metadata)).agent.followup(createUserMessage({
16015
+ this.assertOpen(signal);
16016
+ if (requestId !== void 0) {
16017
+ const log = await this.readLog(metadata, signal);
16018
+ const existingQuestion = postSeedUserQuestionById(log, requestId);
16019
+ if (existingQuestion !== null) {
16020
+ if (existingQuestion !== question) throw new Error("CiteCiter ask requestId was reused for a different question");
16021
+ if (committedPostSeedUserQuestionById(log, requestId) !== null) {
16022
+ const live = this.handles.get(sessionId)?.agent.session;
16023
+ if (live !== void 0) await this.runtime.sessions.flush(live);
16024
+ return this.snapshot(metadata);
16025
+ }
16026
+ }
16027
+ }
16028
+ const handle = await this.ensureHandle(metadata, signal);
16029
+ if (requestId !== void 0) handle.agent.inbox.remove(MessageId(requestId));
16030
+ await this.commitFollowup(handle, requestId === void 0 ? createUserMessage({
15713
16031
  content: [{
15714
16032
  type: "text",
15715
16033
  text: question
15716
16034
  }],
15717
16035
  source: { kind: "user" }
15718
- }));
16036
+ }) : identifiedQuestion(requestId, question), signal);
15719
16037
  const updated = {
15720
16038
  ...metadata,
15721
16039
  updatedAt: Date.now()
@@ -15723,7 +16041,38 @@ var TopicRuntime = class {
15723
16041
  await this.index.save(updated);
15724
16042
  return this.snapshot(updated);
15725
16043
  }
16044
+ async askIdempotent(request, signal) {
16045
+ if (request.requestId === void 0) return this.queueAsk(request, signal);
16046
+ const key = `${request.topicSessionId}\0${request.requestId}`;
16047
+ const existing = this.asks.get(key);
16048
+ if (existing !== void 0) {
16049
+ if (existing.question !== request.question) throw new Error("CiteCiter ask requestId was reused for a different question");
16050
+ return this.waitForCaller(existing.result, signal);
16051
+ }
16052
+ const result = this.queueAsk(request, signal).finally(() => this.asks.delete(key));
16053
+ this.asks.set(key, {
16054
+ question: request.question,
16055
+ result
16056
+ });
16057
+ return this.waitForCaller(result, signal);
16058
+ }
16059
+ queueAsk(request, signal) {
16060
+ return this.queueTopicAdmission(request.topicSessionId, () => this.ask(request.topicSessionId, request.question, request.requestId, signal), signal);
16061
+ }
16062
+ queueTopicAdmission(sessionId, operation, signal) {
16063
+ const result = (this.topicAdmissions.get(sessionId) ?? Promise.resolve()).then(() => {
16064
+ this.assertOpen(signal);
16065
+ return operation();
16066
+ });
16067
+ const settled = result.then(() => void 0, () => void 0);
16068
+ this.topicAdmissions.set(sessionId, settled);
16069
+ settled.then(() => {
16070
+ if (this.topicAdmissions.get(sessionId) === settled) this.topicAdmissions.delete(sessionId);
16071
+ });
16072
+ return result;
16073
+ }
15726
16074
  askUser(request) {
16075
+ if (this.closed) throw new UserQuestionError(CITECITER_SHUTTING_DOWN, "ASK_ABORTED");
15727
16076
  const sessionId = request.agent === void 0 ? void 0 : String(request.agent.session.header.id);
15728
16077
  if (sessionId === void 0 || !this.handles.has(sessionId)) throw new UserQuestionError("CiteCiter cannot identify the asking Topic", "CALLER_NOT_LIVE");
15729
16078
  if (this.pendingQuestions.has(sessionId)) throw new UserQuestionError("this Topic already has a pending question", "DUPLICATE_QUESTION");
@@ -15756,31 +16105,36 @@ var TopicRuntime = class {
15756
16105
  if (request.signal?.aborted === true) onAbort();
15757
16106
  });
15758
16107
  }
15759
- async answerQuestion(request) {
16108
+ async answerQuestion(request, signal) {
15760
16109
  const metadata = await this.index.loadBySessionId(request.topicSessionId);
16110
+ this.assertOpen(signal);
15761
16111
  const pending = this.pendingQuestions.get(request.topicSessionId);
15762
16112
  if (pending === void 0 || pending.key !== request.key) throw new Error("这个提问已结束或已被替换");
15763
16113
  pending.resolve(validatedQuestionAnswer(pending.questions, request.answer));
15764
16114
  return this.snapshot(metadata);
15765
16115
  }
15766
- async cancelQuestion(sessionId, key) {
16116
+ async cancelQuestion(sessionId, key, signal) {
15767
16117
  const metadata = await this.index.loadBySessionId(sessionId);
16118
+ this.assertOpen(signal);
15768
16119
  const pending = this.pendingQuestions.get(sessionId);
15769
16120
  if (pending === void 0 || pending.key !== key) throw new Error("这个提问已结束或已被替换");
15770
16121
  pending.reject(new UserQuestionError("the user cancelled ask_user_question", "ASK_CANCELLED"));
15771
16122
  return this.snapshot(metadata);
15772
16123
  }
15773
- async stop(sessionId) {
16124
+ async stop(sessionId, signal) {
15774
16125
  const metadata = await this.index.loadBySessionId(sessionId);
16126
+ this.assertOpen(signal);
15775
16127
  const agent = this.handles.get(sessionId)?.agent;
15776
16128
  agent?.cancel({ kind: "user" });
15777
16129
  await agent?.whenIdle();
15778
16130
  if (agent !== void 0) await this.runtime.sessions.flush(agent.session);
15779
16131
  return this.snapshot(metadata);
15780
16132
  }
15781
- async rename(sessionId, title) {
16133
+ async rename(sessionId, title, signal) {
15782
16134
  const metadata = await this.index.loadBySessionId(sessionId);
15783
- const handle = await this.ensureHandle(metadata);
16135
+ this.assertOpen(signal);
16136
+ const handle = await this.ensureHandle(metadata, signal);
16137
+ this.assertOpen(signal);
15784
16138
  const renamed = this.runtime.sessionTitle.rename(handle.agent.session, title);
15785
16139
  await this.runtime.sessions.flush(handle.agent.session);
15786
16140
  const updated = {
@@ -15793,25 +16147,29 @@ var TopicRuntime = class {
15793
16147
  await this.index.save(updated);
15794
16148
  return this.snapshot(updated);
15795
16149
  }
15796
- async archive(sessionId, archived) {
16150
+ async archive(sessionId, archived, signal) {
16151
+ const metadata = await this.index.loadBySessionId(sessionId);
16152
+ this.assertOpen(signal);
15797
16153
  const updated = {
15798
- ...await this.index.loadBySessionId(sessionId),
16154
+ ...metadata,
15799
16155
  archivedAt: archived ? Date.now() : null,
15800
16156
  updatedAt: Date.now()
15801
16157
  };
15802
16158
  await this.index.save(updated);
15803
16159
  return this.snapshot(updated);
15804
16160
  }
15805
- async delete(sessionId, confirmSessionId) {
16161
+ async delete(sessionId, confirmSessionId, signal) {
15806
16162
  if (sessionId !== confirmSessionId) throw new Error("Topic deletion confirmation does not match the target Session");
15807
16163
  const metadata = await this.index.loadBySessionId(sessionId);
16164
+ this.assertOpen(signal);
15808
16165
  const pending = this.opening.get(sessionId);
15809
16166
  const handle = this.handles.get(sessionId) ?? (pending === void 0 ? void 0 : await pending);
16167
+ this.assertOpen(signal);
15810
16168
  if (handle !== void 0) {
15811
16169
  await handle.dispose();
15812
16170
  this.handles.delete(sessionId);
15813
16171
  }
15814
- const inspection = await this.runtime.sessionPersistence.inspect(SessionId(sessionId));
16172
+ const inspection = await this.runtime.sessionPersistence.inspect(SessionId(sessionId), signal);
15815
16173
  await this.removeSessionArtifact(inspection.meta);
15816
16174
  await this.index.remove(metadata);
15817
16175
  return sessionId;
@@ -15826,20 +16184,25 @@ var TopicRuntime = class {
15826
16184
  }) !== void 0) await unlink(artifact.path);
15827
16185
  await rmdirIfEmpty(resolve(artifact.path, ".."));
15828
16186
  }
15829
- enqueueModelChange(sessionId, apply) {
16187
+ enqueueModelChange(sessionId, apply, signal) {
15830
16188
  const previous = this.modelChanges.get(sessionId);
15831
16189
  let change;
15832
- change = (previous === void 0 ? Promise.resolve() : previous.catch(() => void 0)).then(apply).finally(() => {
16190
+ change = (previous === void 0 ? Promise.resolve() : previous.catch(() => void 0)).then(() => {
16191
+ this.assertOpen(signal);
16192
+ return apply();
16193
+ }).finally(() => {
15833
16194
  if (this.modelChanges.get(sessionId) === change) this.modelChanges.delete(sessionId);
15834
16195
  });
15835
16196
  this.modelChanges.set(sessionId, change);
15836
16197
  return change;
15837
16198
  }
15838
- setModelRoute(request) {
16199
+ setModelRoute(request, signal) {
15839
16200
  return this.enqueueModelChange(request.topicSessionId, async () => {
15840
16201
  const metadata = await this.index.loadBySessionId(request.topicSessionId);
15841
- await this.host.llm.resolveModelInfo(request.provider, request.model);
15842
- await this.ensureHandle(metadata);
16202
+ this.assertOpen(signal);
16203
+ await this.host.llm.resolveModelInfo(request.provider, request.model, signal);
16204
+ await this.ensureHandle(metadata, signal);
16205
+ this.assertOpen(signal);
15843
16206
  const selection = this.selections.get(metadata.sessionId);
15844
16207
  if (selection === void 0) throw new Error("Topic model selector is unavailable");
15845
16208
  const modelConfig = {
@@ -15859,14 +16222,16 @@ var TopicRuntime = class {
15859
16222
  model: request.model
15860
16223
  };
15861
16224
  return this.snapshot(updated);
15862
- });
16225
+ }, signal);
15863
16226
  }
15864
- setReasoningEffort(request) {
16227
+ setReasoningEffort(request, signal) {
15865
16228
  return this.enqueueModelChange(request.topicSessionId, async () => {
15866
16229
  const metadata = await this.index.loadBySessionId(request.topicSessionId);
15867
- const model = await this.host.llm.resolveModelInfo(metadata.modelConfig.provider, metadata.modelConfig.model);
16230
+ this.assertOpen(signal);
16231
+ const model = await this.host.llm.resolveModelInfo(metadata.modelConfig.provider, metadata.modelConfig.model, signal);
15868
16232
  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);
16233
+ await this.ensureHandle(metadata, signal);
16234
+ this.assertOpen(signal);
15870
16235
  const selection = this.selections.get(metadata.sessionId);
15871
16236
  if (selection === void 0) throw new Error("Topic model selector is unavailable");
15872
16237
  const modelConfig = { ...metadata.modelConfig };
@@ -15884,16 +16249,18 @@ var TopicRuntime = class {
15884
16249
  ...request.reasoningEffort === null ? {} : { reasoningEffort: ReasoningEffortId(request.reasoningEffort) }
15885
16250
  };
15886
16251
  return this.snapshot(updated);
15887
- });
16252
+ }, signal);
15888
16253
  }
15889
- selectModel(request) {
15890
- return this.enqueueModelChange(request.topicSessionId, () => this.applyModelSelection(request));
16254
+ selectModel(request, signal) {
16255
+ return this.enqueueModelChange(request.topicSessionId, () => this.applyModelSelection(request, signal), signal);
15891
16256
  }
15892
- async applyModelSelection(request) {
16257
+ async applyModelSelection(request, signal) {
15893
16258
  const metadata = await this.index.loadBySessionId(request.topicSessionId);
15894
- const model = await this.host.llm.resolveModelInfo(request.provider, request.model);
16259
+ this.assertOpen(signal);
16260
+ const model = await this.host.llm.resolveModelInfo(request.provider, request.model, signal);
15895
16261
  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);
16262
+ await this.ensureHandle(metadata, signal);
16263
+ this.assertOpen(signal);
15897
16264
  const selection = this.selections.get(metadata.sessionId);
15898
16265
  if (selection === void 0) throw new Error("Topic model selector is unavailable");
15899
16266
  const previousModelConfig = { ...metadata.modelConfig };
@@ -15916,13 +16283,16 @@ var TopicRuntime = class {
15916
16283
  };
15917
16284
  return this.snapshot(updated);
15918
16285
  }
15919
- async models() {
16286
+ async models(signal) {
15920
16287
  const providers = [];
15921
16288
  for (const provider of this.host.llm.listProviders()) {
16289
+ this.assertOpen(signal);
15922
16290
  let catalog;
15923
16291
  try {
15924
16292
  catalog = await this.host.llm.listModels(provider.id);
16293
+ this.assertOpen(signal);
15925
16294
  } catch (error) {
16295
+ signal?.throwIfAborted();
15926
16296
  this.host.logger.warn(`CiteCiter could not list models for ${provider.id}`, error);
15927
16297
  catalog = [];
15928
16298
  }
@@ -15930,8 +16300,9 @@ var TopicRuntime = class {
15930
16300
  for (const model of catalog) {
15931
16301
  let resolved;
15932
16302
  try {
15933
- resolved = await this.host.llm.resolveModelInfo(provider.id, model.id);
16303
+ resolved = await this.host.llm.resolveModelInfo(provider.id, model.id, signal);
15934
16304
  } catch (error) {
16305
+ signal?.throwIfAborted();
15935
16306
  this.host.logger.warn(`CiteCiter could not resolve ${provider.id}/${model.id}`, error);
15936
16307
  }
15937
16308
  models.push({
@@ -15952,21 +16323,22 @@ var TopicRuntime = class {
15952
16323
  }
15953
16324
  return providers;
15954
16325
  }
15955
- async list(sourceSessionId, includeArchived) {
16326
+ async list(sourceSessionId, includeArchived, signal) {
15956
16327
  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);
16328
+ this.assertOpen(signal);
16329
+ 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
16330
  }
15959
- async summary(metadata) {
16331
+ async summary(metadata, signal) {
15960
16332
  let current = metadata;
15961
16333
  if (cachedTopicTitle(current) === null && !this.titleHydrated.has(current.sessionId)) {
16334
+ const log = await this.readLog(current, signal);
15962
16335
  this.titleHydrated.add(current.sessionId);
15963
- const log = await this.readLog(current);
15964
16336
  const title = foldTopicTitle(current, log.events);
15965
16337
  if (title !== void 0) current = await this.patchMetadata(current, {
15966
16338
  cachedTitle: title.title,
15967
16339
  cachedTitleSource: titleSourceKind(title),
15968
16340
  cachedTitleEventSeq: title.eventSeq
15969
- });
16341
+ }, signal);
15970
16342
  }
15971
16343
  return this.summaryFromMetadata(current);
15972
16344
  }
@@ -15989,20 +16361,27 @@ var TopicRuntime = class {
15989
16361
  modelConfig: metadata.modelConfig
15990
16362
  };
15991
16363
  }
15992
- async readLog(metadata) {
16364
+ async get(sessionId, signal) {
16365
+ const metadata = await this.index.loadBySessionId(sessionId);
16366
+ this.assertOpen(signal);
16367
+ return this.snapshot(metadata, signal);
16368
+ }
16369
+ async readLog(metadata, signal) {
16370
+ if (signal !== void 0) this.assertOpen(signal);
15993
16371
  const live = this.handles.get(metadata.sessionId)?.agent.session;
15994
16372
  if (live !== void 0) return {
15995
16373
  header: live.header,
15996
16374
  events: live.events
15997
16375
  };
15998
- const inspection = await this.runtime.sessionPersistence.inspect(SessionId(metadata.sessionId));
16376
+ const inspection = await this.runtime.sessionPersistence.inspect(SessionId(metadata.sessionId), signal);
16377
+ if (signal !== void 0) this.assertOpen(signal);
15999
16378
  return {
16000
16379
  header: inspection.meta,
16001
16380
  events: inspection.events
16002
16381
  };
16003
16382
  }
16004
16383
  scheduleSourceAvailabilityCheck(metadata) {
16005
- if (this.sourceAvailability.has(metadata.sourceSessionId) || this.sourceAvailabilityChecks.has(metadata.sourceSessionId)) return;
16384
+ if (this.closed || this.sourceAvailability.has(metadata.sourceSessionId) || this.sourceAvailabilityChecks.has(metadata.sourceSessionId)) return;
16006
16385
  const check = (async () => {
16007
16386
  let available = true;
16008
16387
  try {
@@ -16026,10 +16405,10 @@ var TopicRuntime = class {
16026
16405
  const latest = await this.index.loadBySessionId(metadata.sessionId);
16027
16406
  if (latest.sourceAvailable !== available) await this.patchMetadata(latest, { sourceAvailable: available });
16028
16407
  }
16029
- async snapshot(metadata) {
16408
+ async snapshot(metadata, signal) {
16030
16409
  let current = metadata;
16031
16410
  this.scheduleSourceAvailabilityCheck(current);
16032
- const log = await this.readLog(current);
16411
+ const log = await this.readLog(current, signal);
16033
16412
  const title = foldTopicTitle(current, log.events);
16034
16413
  const latest = log.events.at(-1)?.time ?? metadata.updatedAt;
16035
16414
  const observedThroughSeq = latestObservedSeq(log.events);
@@ -16042,7 +16421,7 @@ var TopicRuntime = class {
16042
16421
  cachedTitleSource,
16043
16422
  cachedTitleEventSeq: title.eventSeq
16044
16423
  }
16045
- });
16424
+ }, signal);
16046
16425
  if (title === void 0) this.scheduleExactTitleRefresh(current, log);
16047
16426
  const pending = this.pendingQuestions.get(current.sessionId);
16048
16427
  return {
@@ -16060,8 +16439,9 @@ var TopicRuntime = class {
16060
16439
  }
16061
16440
  };
16062
16441
  }
16063
- async patchMetadata(metadata, patch) {
16442
+ async patchMetadata(metadata, patch, signal) {
16064
16443
  const latest = await this.index.loadBySessionId(metadata.sessionId);
16444
+ if (signal !== void 0) this.assertOpen(signal);
16065
16445
  const updated = topicMetadataSchema.parse({
16066
16446
  ...latest,
16067
16447
  ...patch
@@ -16070,20 +16450,22 @@ var TopicRuntime = class {
16070
16450
  return updated;
16071
16451
  }
16072
16452
  scheduleExactTitleRefresh(metadata, log) {
16073
- if (metadata.mode !== "exact-fork" || this.titleRefreshAttempted.has(metadata.sessionId) || this.handles.get(metadata.sessionId)?.agent.status === "running") return;
16453
+ if (this.closed || metadata.mode !== "exact-fork" || this.titleRefreshAttempted.has(metadata.sessionId) || this.handles.get(metadata.sessionId)?.agent.status === "running") return;
16074
16454
  const postSeed = log.events.slice(log.header.seedLength ?? 0);
16075
16455
  if (!postSeed.some((event) => event.type === "request/header") || !postSeed.some((event) => event.type === "assistant/message")) return;
16076
16456
  const handle = this.handles.get(metadata.sessionId);
16077
16457
  if (handle === void 0) return;
16078
16458
  this.titleRefreshAttempted.add(metadata.sessionId);
16079
- const refresh = this.runtime.sessionTitle.refresh(handle.agent.session).then(async (title) => {
16459
+ const refresh = this.runtime.sessionTitle.refresh(handle.agent.session, this.lifecycleAbort.signal).then(async (title) => {
16460
+ this.assertOpen(this.lifecycleAbort.signal);
16080
16461
  await this.runtime.sessions.flush(handle.agent.session);
16462
+ this.assertOpen(this.lifecycleAbort.signal);
16081
16463
  if (title === void 0 || title.eventSeq <= (metadata.forkThroughSeq ?? -1)) return;
16082
16464
  await this.patchMetadata(metadata, {
16083
16465
  cachedTitle: title.title,
16084
16466
  cachedTitleSource: titleSourceKind(title),
16085
16467
  cachedTitleEventSeq: title.eventSeq
16086
- });
16468
+ }, this.lifecycleAbort.signal);
16087
16469
  }).catch((error) => {
16088
16470
  if (!this.closed) this.host.logger.warn(`CiteCiter could not title Topic ${metadata.sessionId}`, error);
16089
16471
  }).finally(() => {
@@ -16200,8 +16582,8 @@ let CiteCiterHost = (() => {
16200
16582
  await this.topics.initialize();
16201
16583
  }
16202
16584
  /** Validate and execute one strict Topic command. */
16203
- async request(rawRequest) {
16204
- return this.topics.request(citeCiterRequestSchema.parse(rawRequest));
16585
+ async request(rawRequest, signal) {
16586
+ return this.topics.request(citeCiterRequestSchema.parse(rawRequest), signal);
16205
16587
  }
16206
16588
  };
16207
16589
  })();