@unson/brainbase-mcp 0.23.0 → 0.23.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.
@@ -97,6 +97,8 @@ export interface KnowledgeLookupState {
97
97
  revision: number;
98
98
  status: KnowledgeStatus;
99
99
  started_at: number;
100
+ /** Set when the first validated retrieval action is reserved. Old states may omit it. */
101
+ retrieval_started_at?: number | null;
100
102
  limits: KnowledgeLookupLimits;
101
103
  attempts: KnowledgeAttempt[];
102
104
  required_fields: string[] | null;
@@ -2,7 +2,7 @@ import { createHash } from 'node:crypto';
2
2
  import { KNOWLEDGE_LOOKUP_ALLOWED_FIELD_ROOTS, validateKnowledgeLookupFields, validateKnowledgeLookupFinish, } from './knowledge-lookup.js';
3
3
  const TERMINAL = new Set(['satisfied', 'unresolved', 'needs_user_input', 'cancelled']);
4
4
  const ACTION_KINDS = new Set(['search', 'read', 'follow_relation', 'finish']);
5
- const KNOWLEDGE_LOOKUP_FIELD_GUIDANCE = `required_fieldsは自然文ではなく公開field pathを指定してください(例: content, body, statement, markdown, summary, name, environments.production.endpoint)。許可root: ${[...KNOWLEDGE_LOOKUP_ALLOWED_FIELD_ROOTS].join(', ')}`;
5
+ const KNOWLEDGE_LOOKUP_FIELD_GUIDANCE = `required_fieldsは自然文ではなく公開field pathを指定してください。Graph本文はcontentを使い、bodyはソース投影が明示的に公開する場合だけ指定してください(例: content, body, statement, markdown, summary, name, environments.production.endpoint)。許可root: ${[...KNOWLEDGE_LOOKUP_ALLOWED_FIELD_ROOTS].join(', ')}`;
6
6
  const record = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
7
7
  const canonical = (value) => JSON.stringify(value, (_key, item) => record(item)
8
8
  ? Object.fromEntries(Object.entries(item).sort(([a], [b]) => a.localeCompare(b)))
@@ -20,6 +20,7 @@ export function createKnowledgeLookup({ lookupId, question, now = Date.now(), li
20
20
  revision: 0,
21
21
  status: 'resolving',
22
22
  started_at: now,
23
+ retrieval_started_at: null,
23
24
  limits: {
24
25
  attempts: bounded(limits.attempts, 8, 16),
25
26
  replans: bounded(limits.replans, 4, 8),
@@ -42,8 +43,51 @@ function terminate(state, reason, status = 'unresolved') {
42
43
  state.pending = null;
43
44
  return state;
44
45
  }
46
+ const hasPendingRetrieval = (state) => state.pending !== null && state.pending.kind !== 'finish';
47
+ const retrievalStartedAt = (state) => {
48
+ if (typeof state.retrieval_started_at === 'number' && Number.isFinite(state.retrieval_started_at)) {
49
+ return state.retrieval_started_at;
50
+ }
51
+ // States written before retrieval_started_at was introduced can still be
52
+ // bounded safely once they have evidence of a retrieval reservation.
53
+ if (state.attempts.length > 0 || hasPendingRetrieval(state))
54
+ return state.started_at;
55
+ return null;
56
+ };
57
+ const retrievalPhase = (state) => {
58
+ if (hasPendingRetrieval(state))
59
+ return 'in_flight';
60
+ if (state.attempts.length > 0)
61
+ return 'attempted';
62
+ return 'not_started';
63
+ };
64
+ function recordTimedOutPendingRetrieval(state) {
65
+ const pending = state.pending;
66
+ if (!pending)
67
+ return;
68
+ state.pending = null;
69
+ if (pending.kind === 'finish')
70
+ return;
71
+ if (state.attempts.some((attempt) => attempt.attempt_id === pending.tool_use_id))
72
+ return;
73
+ // The host cannot tell whether a timed-out reservation reached the tool.
74
+ // Keep the reservation as an unknown transport attempt rather than treating
75
+ // a late or missing result as a successful read.
76
+ state.attempts.push({
77
+ attempt_id: pending.tool_use_id,
78
+ kind: pending.kind,
79
+ fingerprint: pending.fingerprint,
80
+ outcome: 'transport_error',
81
+ references: [],
82
+ searched_scope: {},
83
+ missing_fields: state.required_fields ?? []
84
+ });
85
+ state.revision += 1;
86
+ }
45
87
  function expire(state, now) {
46
- if (!isKnowledgeLookupTerminal(state) && now - state.started_at >= state.limits.milliseconds) {
88
+ const startedAt = retrievalStartedAt(state);
89
+ if (startedAt !== null && !isKnowledgeLookupTerminal(state) && now - startedAt >= state.limits.milliseconds) {
90
+ recordTimedOutPendingRetrieval(state);
47
91
  terminate(state, 'time_budget_exhausted');
48
92
  }
49
93
  return state;
@@ -196,8 +240,10 @@ function validFinish(state, action) {
196
240
  }
197
241
  export function prepareKnowledgeAction(previous, input, toolUseId, now = Date.now()) {
198
242
  const state = expire(structuredClone(previous), now);
199
- if (isKnowledgeLookupTerminal(state))
243
+ if (isKnowledgeLookupTerminal(state)) {
244
+ recordTimedOutPendingRetrieval(state);
200
245
  return { state, allowed: false, reason: state.termination_reason ?? state.status };
246
+ }
201
247
  if (state.pending)
202
248
  return { state, allowed: false, reason: 'attempt_in_flight' };
203
249
  if (!record(input))
@@ -312,6 +358,9 @@ export function prepareKnowledgeAction(previous, input, toolUseId, now = Date.no
312
358
  }
313
359
  state.replans += 1;
314
360
  }
361
+ if (state.retrieval_started_at == null && state.attempts.length === 0 && !hasPendingRetrieval(state)) {
362
+ state.retrieval_started_at = now;
363
+ }
315
364
  state.pending = {
316
365
  tool_use_id: toolUseId,
317
366
  kind: action.kind,
@@ -323,6 +372,14 @@ export function prepareKnowledgeAction(previous, input, toolUseId, now = Date.no
323
372
  }
324
373
  export function recordKnowledgeResult(previous, input, result, toolUseId, now = Date.now()) {
325
374
  const state = structuredClone(previous);
375
+ if (isKnowledgeLookupTerminal(state)) {
376
+ // A late PostToolUse must never reopen a finished lookup. Preserve a stale
377
+ // retrieval reservation as unknown while keeping the terminal state.
378
+ recordTimedOutPendingRetrieval(state);
379
+ return state;
380
+ }
381
+ if (isKnowledgeLookupTerminal(expire(state, now)))
382
+ return state;
326
383
  if (typeof toolUseId === 'string' && state.finished_calls.includes(toolUseId))
327
384
  return state;
328
385
  if (!state.pending ||
@@ -411,8 +468,10 @@ export function recordKnowledgeResult(previous, input, result, toolUseId, now =
411
468
  }
412
469
  export function stopKnowledgeLookup(previous, now = Date.now()) {
413
470
  const state = expire(structuredClone(previous), now);
414
- if (isKnowledgeLookupTerminal(state))
471
+ if (isKnowledgeLookupTerminal(state)) {
472
+ recordTimedOutPendingRetrieval(state);
415
473
  return { state, block: false, reason: knowledgeLookupContext(state) };
474
+ }
416
475
  state.stop_requests += 1;
417
476
  if (state.stop_requests > state.limits.replans) {
418
477
  terminate(state, 'model_did_not_produce_executable_plan');
@@ -444,6 +503,7 @@ export function knowledgeLookupContext(state) {
444
503
  revision: state.revision,
445
504
  question: state.question,
446
505
  status: state.status,
506
+ retrieval_phase: retrievalPhase(state),
447
507
  required_fields: state.required_fields,
448
508
  attempts: state.attempts,
449
509
  remaining_attempts: Math.max(0, state.limits.attempts - state.attempts.length),
@@ -678,7 +678,7 @@ export const knowledgeLookupTools = [{
678
678
  type: 'array',
679
679
  minItems: 1,
680
680
  maxItems: MAX_FIELDS,
681
- description: 'Public field paths only; use roots such as content, body, statement, markdown, summary, name, or environments.production.endpoint.',
681
+ description: 'Public field paths only; for Graph text use content. Use body only when the source projection explicitly exposes a body field. Other roots include statement, markdown, summary, name, or environments.production.endpoint.',
682
682
  items: { type: 'string', minLength: 1, maxLength: MAX_FIELD_LENGTH },
683
683
  },
684
684
  known_entity_id: { type: 'string', minLength: 1, maxLength: 1_000 },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unson/brainbase-mcp",
3
- "version": "0.23.0",
3
+ "version": "0.23.2",
4
4
  "description": "Local-first Brainbase judgment OS that lets supported AI agents reuse a user's approved goals, priorities, decisions, and judgment criteria.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -407,5 +407,5 @@
407
407
  "engines": {
408
408
  "node": ">=20"
409
409
  },
410
- "gitHead": "25fb5aa910ba704d704768d3df5f271b085df5a2"
410
+ "gitHead": "8e33db88f3076894ebd280f94bd9b716fee4ef53"
411
411
  }