@amoshydra/oktoso 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/hub.js CHANGED
@@ -21289,7 +21289,8 @@ var zSendAnswer = external_exports.object({
21289
21289
  });
21290
21290
  var zSendInput = external_exports.object({
21291
21291
  text: external_exports.string().min(1),
21292
- answer: zSendAnswer.optional()
21292
+ answer: zSendAnswer.optional(),
21293
+ answers: external_exports.array(zSendAnswer).min(1).max(20).optional()
21293
21294
  });
21294
21295
  var zSendResult = external_exports.object({
21295
21296
  accepted: external_exports.boolean(),
@@ -21342,6 +21343,20 @@ var zQuestionPrompt = external_exports.object({
21342
21343
  var zQuestionResponse = external_exports.object({
21343
21344
  data: zQuestionPrompt.nullable()
21344
21345
  });
21346
+ var zPendingQuestionSummary = external_exports.object({
21347
+ sessionRef: external_exports.string(),
21348
+ messageId: external_exports.string(),
21349
+ revision: external_exports.string(),
21350
+ askedAt: external_exports.number().int(),
21351
+ questionCount: external_exports.number().int().min(1),
21352
+ text: external_exports.string(),
21353
+ context: external_exports.string().optional(),
21354
+ attentionID: external_exports.string().optional()
21355
+ });
21356
+ var zQuestionSummaryPage = external_exports.object({
21357
+ data: external_exports.array(zPendingQuestionSummary),
21358
+ failed: external_exports.array(external_exports.string()).optional()
21359
+ });
21345
21360
 
21346
21361
  // packages/contracts/src/events.ts
21347
21362
  var zHubEventType = external_exports.enum([
@@ -26769,7 +26784,10 @@ function splitSentences(value) {
26769
26784
  return (normalized.match(/[^.!?]+[.!?]+|[^.!?]+$/g) ?? []).map((sentence) => sentence.trim()).filter(Boolean);
26770
26785
  }
26771
26786
  function cleanQuestion(value) {
26772
- return value.trim().replace(/^[“"'‘]+/, "").replace(/[”"'’]+$/, "").replace(/\s+/g, " ").trim();
26787
+ return value.trim().replace(/^[“"'‘]+/, "").replace(/[”"'’]+$/, "").replace(/^(?:\d+[.)]|[A-Za-z][.)]|[-*•])\s+/, "").replace(/\s+/g, " ").trim();
26788
+ }
26789
+ function questionCore(value) {
26790
+ return value.replace(/^(?:and|also|then|so)\s+/i, "").trim();
26773
26791
  }
26774
26792
  function truncate2(value, max) {
26775
26793
  const clean = value.replace(/\s+/g, " ").trim();
@@ -26814,7 +26832,8 @@ function listGroups(lines) {
26814
26832
  for (let index = 0; index < lines.length; index += 1) {
26815
26833
  const line = lines[index] ?? "";
26816
26834
  const item = parseListLine(line);
26817
- if (item) {
26835
+ const questionItem = item ? /\?\s*$/.test(item) && !COURTESY.test(item) : false;
26836
+ if (item && !questionItem) {
26818
26837
  if (!current) {
26819
26838
  current = { start: index, end: index, items: [] };
26820
26839
  }
@@ -26910,15 +26929,15 @@ function makeChoice(messageID, questionIndex, index, text, options) {
26910
26929
  ...risk.sensitive ? { sensitive: true } : {}
26911
26930
  };
26912
26931
  }
26913
- function buildQuestion(message, questionText, context, lines, rawText) {
26914
- const items = listItemsNearQuestion(lines, questionText);
26932
+ function buildQuestion(message, questionText, context, items, rawText, questionIndex) {
26915
26933
  let choices = [];
26916
26934
  let kind = "freeform";
26917
26935
  let source = "heuristic";
26918
26936
  let confidence = "high";
26937
+ const core = questionCore(questionText);
26919
26938
  if (items) {
26920
26939
  choices = items.map(
26921
- (item, index) => makeChoice(message.id, 0, index, item, {
26940
+ (item, index) => makeChoice(message.id, questionIndex, index, item, {
26922
26941
  kind: "choice",
26923
26942
  source: "explicit"
26924
26943
  })
@@ -26927,10 +26946,10 @@ function buildQuestion(message, questionText, context, lines, rawText) {
26927
26946
  source = "explicit";
26928
26947
  confidence = "high";
26929
26948
  } else {
26930
- const alternatives = inlineAlternatives(questionText) ?? (/\b(which|what|choose|prefer|option)\b/i.test(questionText) ? contextAlternatives(context) : void 0);
26949
+ const alternatives = inlineAlternatives(core) ?? (/\b(which|what|choose|prefer|option)\b/i.test(core) ? contextAlternatives(context) : void 0);
26931
26950
  if (alternatives) {
26932
26951
  choices = alternatives.map(
26933
- (item, index) => makeChoice(message.id, 0, index, item, {
26952
+ (item, index) => makeChoice(message.id, questionIndex, index, item, {
26934
26953
  kind: "choice",
26935
26954
  source: "heuristic"
26936
26955
  })
@@ -26938,15 +26957,15 @@ function buildQuestion(message, questionText, context, lines, rawText) {
26938
26957
  kind = "choice";
26939
26958
  source = "heuristic";
26940
26959
  confidence = "medium";
26941
- } else if (AUXILIARY.test(questionText) && !INFORMATION_REQUEST.test(questionText)) {
26960
+ } else if (AUXILIARY.test(core) && !INFORMATION_REQUEST.test(core)) {
26942
26961
  const confirm = isDestructive(questionText) || isDestructive(rawText);
26943
26962
  choices = [
26944
- makeChoice(message.id, 0, 0, "Yes", {
26963
+ makeChoice(message.id, questionIndex, 0, "Yes", {
26945
26964
  kind: "boolean",
26946
26965
  source: "heuristic",
26947
26966
  confirm
26948
26967
  }),
26949
- makeChoice(message.id, 0, 1, "No", {
26968
+ makeChoice(message.id, questionIndex, 1, "No", {
26950
26969
  kind: "boolean",
26951
26970
  source: "heuristic"
26952
26971
  })
@@ -26958,7 +26977,7 @@ function buildQuestion(message, questionText, context, lines, rawText) {
26958
26977
  }
26959
26978
  const risk = riskFor(questionText, choices, rawText);
26960
26979
  return {
26961
- id: `${message.id}:q0`,
26980
+ id: `${message.id}:q${questionIndex}`,
26962
26981
  text: questionText,
26963
26982
  ...context ? { context } : {},
26964
26983
  kind,
@@ -26983,6 +27002,70 @@ function choiceCueLine(lines) {
26983
27002
  }
26984
27003
  return void 0;
26985
27004
  }
27005
+ function isQuotedQuestion(sentence) {
27006
+ return /(?:asked|said|wrote|quoted)\s*[:,]?\s*[“"]/i.test(sentence);
27007
+ }
27008
+ function questionEntries(lines) {
27009
+ const entries = [];
27010
+ const seen = /* @__PURE__ */ new Set();
27011
+ const add = (text, line) => {
27012
+ if (!text || seen.has(text)) return;
27013
+ seen.add(text);
27014
+ entries.push({ text, line });
27015
+ };
27016
+ for (const [index, line] of lines.entries()) {
27017
+ const item = parseListLine(line);
27018
+ if (!item) continue;
27019
+ if (!/\?\s*$/.test(item) || COURTESY.test(item)) continue;
27020
+ if (isQuotedQuestion(line)) continue;
27021
+ add(cleanQuestion(item), index);
27022
+ }
27023
+ const prose = lines.filter(
27024
+ (line) => !parseListLine(line) && !/^\s*[-*]\s+\[[ xX]\]/.test(line)
27025
+ ).join(" ");
27026
+ for (const sentence of splitSentences(prose)) {
27027
+ const trimmed = sentence.trim();
27028
+ if (!/\?\s*$/.test(trimmed) || COURTESY.test(trimmed)) continue;
27029
+ if (isQuotedQuestion(trimmed)) continue;
27030
+ const text = cleanQuestion(trimmed);
27031
+ if (!text) continue;
27032
+ const prefix = text.slice(0, Math.min(24, text.length)).toLowerCase();
27033
+ const line = lines.findIndex(
27034
+ (value) => value.toLowerCase().includes(prefix)
27035
+ );
27036
+ add(text, line >= 0 ? line : 0);
27037
+ }
27038
+ return entries.sort((left, right) => left.line - right.line);
27039
+ }
27040
+ function multipleChoiceItems(lines, entry, nextLine, previousLine, groups, used) {
27041
+ const after = groups.find(
27042
+ (group2) => !used.has(group2.start) && group2.start > entry.line && group2.start <= entry.line + 3 && group2.end < nextLine
27043
+ );
27044
+ const before = groups.find(
27045
+ (group2) => !used.has(group2.start) && group2.end < entry.line && entry.line - group2.end <= 3 && group2.start > previousLine
27046
+ );
27047
+ const group = after ?? before;
27048
+ if (!group) return void 0;
27049
+ if (group.items.length < 2 || group.items.length > 6) return void 0;
27050
+ if (group.items.some((item) => item.length < 2 || item.length > 80)) {
27051
+ return void 0;
27052
+ }
27053
+ if (group.items.some(
27054
+ (item) => /\?\s*$/.test(item) && !COURTESY.test(item)
27055
+ )) {
27056
+ return void 0;
27057
+ }
27058
+ const core = questionCore(entry.text);
27059
+ const cue = /\b(which|choose|pick|prefer|option|one of)\b/i.test(core) || /:\s*$/.test(entry.text);
27060
+ if (!cue && AUXILIARY.test(core)) {
27061
+ if (group.items.some((item) => IMPERATIVE.test(item))) return void 0;
27062
+ if (group.items.some((item) => item.split(/\s+/).length > 8)) {
27063
+ return void 0;
27064
+ }
27065
+ }
27066
+ used.add(group.start);
27067
+ return group.items;
27068
+ }
26986
27069
  function parseQuestion(messages, options) {
26987
27070
  const latest = messages[0];
26988
27071
  if (!latest || latest.role !== "assistant" || latest.streaming === true) {
@@ -26999,68 +27082,116 @@ function parseQuestion(messages, options) {
26999
27082
  const paragraph = paragraphs[paragraphIndex];
27000
27083
  if (!paragraph) return null;
27001
27084
  const localLines = stripNonProseLines(stripFencedCode(raw2)).split("\n");
27002
- const proseParagraph = paragraph.split("\n").filter(
27085
+ const paragraphLines = paragraph.split("\n");
27086
+ const entries = questionEntries(paragraphLines);
27087
+ const proseParagraph = paragraphLines.filter(
27003
27088
  (line) => !parseListLine(line) && !/^\s*[-*]\s+\[[ xX]\]/.test(line)
27004
27089
  ).join("\n").trim();
27005
27090
  const sentences = splitSentences(proseParagraph || paragraph);
27006
- const questionIndexes = sentences.map((sentence, index) => ({ sentence, index })).filter(
27007
- ({ sentence }) => /\?\s*$/.test(sentence) && !COURTESY.test(sentence.trim())
27008
- );
27009
- if (questionIndexes.length > 1) {
27010
- return null;
27011
- }
27012
- let question = "";
27013
- let context;
27014
- if (questionIndexes.length === 1) {
27015
- const questionIndex = questionIndexes[0].index;
27016
- question = cleanQuestion(sentences[questionIndex] ?? "");
27017
- if (!question) return null;
27018
- const questionSentence = sentences[questionIndex] ?? "";
27019
- if (/(?:asked|said|wrote|quoted)\s*[:,]?\s*[“"]/i.test(questionSentence)) {
27020
- return null;
27091
+ let questions;
27092
+ if (entries.length > 1) {
27093
+ if (entries.length > 20) return null;
27094
+ const groups = listGroups(paragraphLines);
27095
+ const used = /* @__PURE__ */ new Set();
27096
+ questions = [];
27097
+ for (const [index, entry] of entries.entries()) {
27098
+ const nextLine = entries[index + 1]?.line ?? paragraphLines.length;
27099
+ const previousLine = entries[index - 1]?.line ?? -1;
27100
+ const items = multipleChoiceItems(
27101
+ paragraphLines,
27102
+ entry,
27103
+ nextLine,
27104
+ previousLine,
27105
+ groups,
27106
+ used
27107
+ );
27108
+ const pending = buildQuestion(
27109
+ latest,
27110
+ entry.text,
27111
+ void 0,
27112
+ items,
27113
+ raw2,
27114
+ index
27115
+ );
27116
+ if (!pending) return null;
27117
+ questions.push(pending);
27021
27118
  }
27022
- const trailing = sentences.slice(questionIndex + 1).filter(
27119
+ } else if (entries.length === 1) {
27120
+ const entry = entries[0];
27121
+ const questionIndex = sentences.findIndex(
27122
+ (sentence) => cleanQuestion(sentence) === entry.text
27123
+ );
27124
+ const trailing = questionIndex >= 0 ? sentences.slice(questionIndex + 1).filter(
27023
27125
  (sentence) => !/^(let me know|thanks|thank you|cheers|please let me know)\b/i.test(
27024
27126
  sentence
27025
27127
  )
27026
- );
27128
+ ) : [];
27027
27129
  if (trailing.length > 2) return null;
27028
- const previous = sentences[questionIndex - 1]?.trim();
27029
- context = previous && previous.length <= 120 && !COURTESY.test(previous) ? previous : void 0;
27130
+ const previous = questionIndex > 0 ? sentences[questionIndex - 1]?.trim() : void 0;
27131
+ const context = previous && previous.length <= 120 && !COURTESY.test(previous) ? previous : void 0;
27132
+ const pending = buildQuestion(
27133
+ latest,
27134
+ entry.text,
27135
+ context,
27136
+ listItemsNearQuestion(localLines, entry.text),
27137
+ raw2,
27138
+ 0
27139
+ );
27140
+ if (!pending) return null;
27141
+ questions = [pending];
27030
27142
  } else {
27031
- question = choiceCueLine(localLines) ?? "";
27143
+ const question = choiceCueLine(localLines) ?? "";
27032
27144
  if (!question) return null;
27145
+ const pending = buildQuestion(
27146
+ latest,
27147
+ question,
27148
+ void 0,
27149
+ listItemsNearQuestion(localLines, question),
27150
+ raw2,
27151
+ 0
27152
+ );
27153
+ if (!pending) return null;
27154
+ questions = [pending];
27033
27155
  }
27034
- const pending = buildQuestion(
27035
- latest,
27036
- question,
27037
- context,
27038
- localLines,
27039
- raw2
27040
- );
27041
- if (!pending) return null;
27042
27156
  const hash2 = createHash("sha256").update(raw2).digest("hex").slice(0, 8);
27043
27157
  return {
27044
27158
  sessionRef: options.sessionRef,
27045
27159
  messageId: latest.id,
27046
27160
  revision: `${latest.id}:${hash2}`,
27047
27161
  askedAt: latest.createdAt,
27048
- questions: [pending],
27162
+ questions,
27049
27163
  ...options.attentionID ? { attentionID: options.attentionID } : {},
27050
27164
  generatedAt: options.now ?? Date.now()
27051
27165
  };
27052
27166
  }
27053
27167
 
27054
27168
  // packages/hub/src/services/question-service.ts
27169
+ var MAX_SUMMARY_REFS = 10;
27170
+ function summarize(prompt) {
27171
+ const first = prompt.questions[0];
27172
+ return {
27173
+ sessionRef: prompt.sessionRef,
27174
+ messageId: prompt.messageId,
27175
+ revision: prompt.revision,
27176
+ askedAt: prompt.askedAt,
27177
+ questionCount: prompt.questions.length,
27178
+ text: first.text,
27179
+ ...first.context ? { context: first.context } : {},
27180
+ ...prompt.attentionID ? { attentionID: prompt.attentionID } : {}
27181
+ };
27182
+ }
27055
27183
  var QuestionService = class {
27056
27184
  constructor(sessions, options = {}) {
27057
27185
  this.sessions = sessions;
27058
27186
  this.ttlMs = options.ttlMs ?? 3e4;
27187
+ this.summaryTtlMs = options.summaryTtlMs ?? 15e3;
27059
27188
  this.now = options.now ?? Date.now;
27060
27189
  }
27061
27190
  sessions;
27062
27191
  cache = /* @__PURE__ */ new Map();
27192
+ summaryCache = /* @__PURE__ */ new Map();
27063
27193
  ttlMs;
27194
+ summaryTtlMs;
27064
27195
  now;
27065
27196
  async get(ref) {
27066
27197
  const page = await this.sessions.messages(ref, {
@@ -27068,13 +27199,22 @@ var QuestionService = class {
27068
27199
  order: "desc"
27069
27200
  });
27070
27201
  const latest = page.data[0];
27071
- if (!latest) return null;
27072
- if (latest.streaming === true) return null;
27202
+ if (!latest) {
27203
+ this.rememberSummary(ref, null);
27204
+ return null;
27205
+ }
27206
+ if (latest.streaming === true) {
27207
+ this.rememberSummary(ref, null);
27208
+ return null;
27209
+ }
27073
27210
  const text = latest.parts.filter((part) => part.type === "text").map((part) => part.text).join("\n");
27074
27211
  const hash2 = createHash2("sha256").update(text).digest("hex").slice(0, 8);
27075
27212
  const key = `${ref}:${latest.id}:${hash2}`;
27076
27213
  const cached2 = this.cache.get(key);
27077
- if (cached2 && cached2.expiresAt > this.now()) return cached2.value;
27214
+ if (cached2 && cached2.expiresAt > this.now()) {
27215
+ this.rememberSummary(ref, cached2.value);
27216
+ return cached2.value;
27217
+ }
27078
27218
  const value = parseQuestion(page.data, {
27079
27219
  sessionRef: ref,
27080
27220
  now: this.now()
@@ -27087,10 +27227,58 @@ var QuestionService = class {
27087
27227
  const oldest = this.cache.keys().next().value;
27088
27228
  if (oldest) this.cache.delete(oldest);
27089
27229
  }
27230
+ this.rememberSummary(ref, value);
27090
27231
  return value;
27091
27232
  }
27233
+ /**
27234
+ * Cheap discovery for the glass home and inbox. Results are cached briefly
27235
+ * and provider failures are reported separately so callers do not treat an
27236
+ * outage as a resolved question.
27237
+ */
27238
+ async summaries(refs) {
27239
+ const unique = [...new Set(refs.map((ref) => ref.trim()).filter(Boolean))];
27240
+ const limited = unique.slice(0, MAX_SUMMARY_REFS);
27241
+ const failed = [];
27242
+ const results = await Promise.all(
27243
+ limited.map(async (ref) => {
27244
+ const cached2 = this.summaryCache.get(ref);
27245
+ if (cached2 && cached2.expiresAt > this.now()) return cached2.value;
27246
+ try {
27247
+ const prompt = await this.get(ref);
27248
+ return prompt ? summarize(prompt) : null;
27249
+ } catch {
27250
+ failed.push(ref);
27251
+ return null;
27252
+ }
27253
+ })
27254
+ );
27255
+ return {
27256
+ data: results.filter(
27257
+ (item) => item !== null
27258
+ ),
27259
+ failed
27260
+ };
27261
+ }
27092
27262
  clear() {
27093
27263
  this.cache.clear();
27264
+ this.summaryCache.clear();
27265
+ }
27266
+ invalidate(ref) {
27267
+ if (ref) {
27268
+ this.summaryCache.delete(ref);
27269
+ return;
27270
+ }
27271
+ this.summaryCache.clear();
27272
+ }
27273
+ rememberSummary(ref, prompt) {
27274
+ this.summaryCache.set(ref, {
27275
+ value: prompt ? summarize(prompt) : null,
27276
+ expiresAt: this.now() + this.summaryTtlMs
27277
+ });
27278
+ if (this.summaryCache.size > 100) {
27279
+ const oldest = this.summaryCache.keys().next().value;
27280
+ if (oldest) this.summaryCache.delete(oldest);
27281
+ }
27094
27282
  }
27095
27283
  };
27096
27284
 
@@ -29256,7 +29444,7 @@ var IdempotencyService = class {
29256
29444
  };
29257
29445
 
29258
29446
  // package.json
29259
- var version2 = "0.1.3";
29447
+ var version2 = "0.1.4";
29260
29448
 
29261
29449
  // packages/hub/src/version.ts
29262
29450
  var VERSION = version2;
@@ -29843,7 +30031,49 @@ function registerSessionRoutes(app, sessions, replies, questions, idempotency) {
29843
30031
  }
29844
30032
  }
29845
30033
  const answer = parsed.data.answer;
29846
- if (answer) {
30034
+ const answers = parsed.data.answers;
30035
+ if (answer && answers) {
30036
+ throw badRequest("Use either answer or answers, not both");
30037
+ }
30038
+ if (answers) {
30039
+ const prompt = await questions.get(ref);
30040
+ if (!prompt) {
30041
+ throw conflict("This question is no longer pending", {
30042
+ reason: "question_resolved"
30043
+ });
30044
+ }
30045
+ if (answers.some(
30046
+ (item) => item.messageId !== prompt.messageId || item.revision !== prompt.revision
30047
+ )) {
30048
+ throw conflict("Question changed", {
30049
+ reason: "question_changed",
30050
+ current: prompt
30051
+ });
30052
+ }
30053
+ const known = new Set(prompt.questions.map((question) => question.id));
30054
+ const seen = /* @__PURE__ */ new Set();
30055
+ for (const item of answers) {
30056
+ if (!known.has(item.questionId)) {
30057
+ throw conflict("Question changed", {
30058
+ reason: "question_changed",
30059
+ current: prompt
30060
+ });
30061
+ }
30062
+ if (seen.has(item.questionId)) {
30063
+ throw badRequest("Duplicate question in answer batch");
30064
+ }
30065
+ seen.add(item.questionId);
30066
+ }
30067
+ const missing = prompt.questions.filter(
30068
+ (question) => !seen.has(question.id)
30069
+ );
30070
+ if (missing.length > 0) {
30071
+ throw conflict("Answer every question in this batch", {
30072
+ reason: "question_incomplete",
30073
+ current: prompt
30074
+ });
30075
+ }
30076
+ } else if (answer) {
29847
30077
  const prompt = await questions.get(ref);
29848
30078
  if (!prompt) {
29849
30079
  throw conflict("This question is no longer pending", {
@@ -29861,6 +30091,7 @@ function registerSessionRoutes(app, sessions, replies, questions, idempotency) {
29861
30091
  }
29862
30092
  }
29863
30093
  const result = await sessions.sendMessage(ref, { text: parsed.data.text });
30094
+ questions.invalidate(ref);
29864
30095
  if (cacheKey2) await idempotency.set(cacheKey2, result);
29865
30096
  return c.json(result, 202);
29866
30097
  });
@@ -29880,7 +30111,38 @@ function registerSessionRoutes(app, sessions, replies, questions, idempotency) {
29880
30111
  }
29881
30112
 
29882
30113
  // packages/hub/src/http/routes/questions.ts
30114
+ var MAX_SUMMARY_REFS2 = 10;
30115
+ function summaryRefs(values) {
30116
+ const refs = (values ?? []).flatMap((value) => value.split(",")).map((value) => value.trim()).filter(Boolean);
30117
+ return [...new Set(refs)];
30118
+ }
29883
30119
  function registerQuestionRoutes(app, questions, attention) {
30120
+ app.get("/api/v1/questions", async (c) => {
30121
+ const refs = summaryRefs(c.req.queries("ref"));
30122
+ if (refs.length === 0) {
30123
+ throw badRequest("At least one session ref is required");
30124
+ }
30125
+ if (refs.length > MAX_SUMMARY_REFS2) {
30126
+ throw badRequest(`At most ${MAX_SUMMARY_REFS2} session refs are allowed`);
30127
+ }
30128
+ if (refs.some((ref) => ref.length > 256)) {
30129
+ throw badRequest("Session ref is too long");
30130
+ }
30131
+ const [summaryResult, attentionPage] = await Promise.all([
30132
+ questions.summaries(refs),
30133
+ attention.list({ type: "question" })
30134
+ ]);
30135
+ const attentionBySession = new Map(
30136
+ attentionPage.data.map((item) => [item.sessionRef, item.id])
30137
+ );
30138
+ return c.json({
30139
+ data: summaryResult.data.map((summary) => {
30140
+ const attentionID = attentionBySession.get(summary.sessionRef);
30141
+ return attentionID ? { ...summary, attentionID } : summary;
30142
+ }),
30143
+ failed: summaryResult.failed
30144
+ });
30145
+ });
29884
30146
  app.get("/api/v1/sessions/:ref/question", async (c) => {
29885
30147
  const ref = c.req.param("ref");
29886
30148
  if (!ref) throw notFound("Session not found");