@sanctuary-framework/mcp-server 1.2.6 → 1.2.7

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/index.js CHANGED
@@ -4915,7 +4915,8 @@ var SIGNATURE_SCHEME_V1 = "ed25519-v1";
4915
4915
  var RESERVED_EVENT_TYPE_PREFIXES = [
4916
4916
  "EXTENSION_",
4917
4917
  "cross_fortress_",
4918
- "multi_master_"
4918
+ "multi_master_",
4919
+ "cross_harness_approval_"
4919
4920
  ];
4920
4921
  function isReservedEventType(s) {
4921
4922
  return RESERVED_EVENT_TYPE_PREFIXES.some((p) => s.startsWith(p));
@@ -9095,9 +9096,9 @@ function fingerprintDID(did) {
9095
9096
  return `${raw.slice(0, 6)}\u2026${raw.slice(-6)}`;
9096
9097
  }
9097
9098
  function countInjectionsToday(audit) {
9098
- const startOfDay = /* @__PURE__ */ new Date();
9099
- startOfDay.setHours(0, 0, 0, 0);
9100
- const cutoff = startOfDay.getTime();
9099
+ const startOfDay2 = /* @__PURE__ */ new Date();
9100
+ startOfDay2.setHours(0, 0, 0, 0);
9101
+ const cutoff = startOfDay2.getTime();
9101
9102
  return audit.filter((e) => {
9102
9103
  const ts = new Date(e.timestamp).getTime();
9103
9104
  if (isNaN(ts) || ts < cutoff) return false;
@@ -9111,9 +9112,9 @@ var PROOF_CREATION_OPS = /* @__PURE__ */ new Set([
9111
9112
  "proof_commitment"
9112
9113
  ]);
9113
9114
  function countProofsToday(audit) {
9114
- const startOfDay = /* @__PURE__ */ new Date();
9115
- startOfDay.setHours(0, 0, 0, 0);
9116
- const cutoff = startOfDay.getTime();
9115
+ const startOfDay2 = /* @__PURE__ */ new Date();
9116
+ startOfDay2.setHours(0, 0, 0, 0);
9117
+ const cutoff = startOfDay2.getTime();
9117
9118
  return audit.filter((e) => {
9118
9119
  if (e.layer !== "l3") return false;
9119
9120
  if (!PROOF_CREATION_OPS.has(e.operation)) return false;
@@ -16312,6 +16313,24 @@ async function handleApprovalInboxRoute(deps, req, res) {
16312
16313
  await handleStream2(deps, res);
16313
16314
  return true;
16314
16315
  }
16316
+ if (method === "GET" && path === `${APPROVAL_INBOX_API_PREFIX}/revision`) {
16317
+ const revision = await deps.aggregator.getRevision();
16318
+ writeJSON4(res, 200, { ok: true, data: { revision } });
16319
+ return true;
16320
+ }
16321
+ if (method === "GET" && path === `${APPROVAL_INBOX_API_PREFIX}/sync`) {
16322
+ const sinceRaw = url.searchParams.get("since_revision");
16323
+ const sinceParsed = sinceRaw === null ? 0 : Number.parseInt(sinceRaw, 10);
16324
+ const sinceRevision = Number.isFinite(sinceParsed) && sinceParsed >= 0 ? sinceParsed : 0;
16325
+ const limit = parseLimit2(
16326
+ url.searchParams.get("limit"),
16327
+ APPROVAL_INBOX_DEFAULT_LIMIT,
16328
+ APPROVAL_INBOX_MAX_LIMIT
16329
+ );
16330
+ const delta = await deps.aggregator.getSync({ sinceRevision, limit });
16331
+ writeJSON4(res, 200, { ok: true, data: delta });
16332
+ return true;
16333
+ }
16315
16334
  if (method === "GET" && path === `${APPROVAL_INBOX_API_PREFIX}/history`) {
16316
16335
  const limit = parseLimit2(
16317
16336
  url.searchParams.get("limit"),
@@ -19407,6 +19426,20 @@ var ApprovalAggregator = class {
19407
19426
  hydrated = false;
19408
19427
  /** Active SSE listeners. */
19409
19428
  listeners = /* @__PURE__ */ new Set();
19429
+ /**
19430
+ * Monotonic revision counter, bumped on every mutation (ingest of new
19431
+ * entry, resolve, expire, delete). Hydrated from max(last_modified_revision)
19432
+ * across persisted entries on first read; in-memory after that. v1.3
19433
+ * Upsilon-4.
19434
+ */
19435
+ currentRevision = 0;
19436
+ /**
19437
+ * Removal tombstones: aggregator_id -> revision at removal. Used by the
19438
+ * sync API to surface "removed" entries to mobile consumers between
19439
+ * polls. In-memory only; server restart clears tombstones (mobile
19440
+ * bootstraps via `list()` on reconnect). v1.3 Upsilon-4.
19441
+ */
19442
+ removedTombstones = /* @__PURE__ */ new Map();
19410
19443
  constructor(deps) {
19411
19444
  this.storage = deps.storage;
19412
19445
  this.encryptionKey = derivePurposeKey(
@@ -19441,6 +19474,113 @@ var ApprovalAggregator = class {
19441
19474
  this.listeners.add(listener);
19442
19475
  return () => this.listeners.delete(listener);
19443
19476
  }
19477
+ /**
19478
+ * Current aggregator revision. v1.3 Upsilon-4. Mobile companions
19479
+ * poll the lightweight `/revision` route to detect that something
19480
+ * changed before fetching a full sync delta.
19481
+ */
19482
+ async getRevision() {
19483
+ await this.hydrate();
19484
+ return this.currentRevision;
19485
+ }
19486
+ /**
19487
+ * Compute a delta since `sinceRevision`. v1.3 Upsilon-4. Mobile
19488
+ * clients poll this for cheap state-sync. Behavior:
19489
+ * - `added`: entries whose `created_at_revision > sinceRevision`.
19490
+ * - `changed`: entries that existed at `sinceRevision` but had a
19491
+ * status transition (resolve, expire) since.
19492
+ * - `removed`: aggregator_ids deleted after `sinceRevision`.
19493
+ * - `revision`: current aggregator revision; pass this back as
19494
+ * `sinceRevision` on the next call.
19495
+ *
19496
+ * `limit` caps the total count returned across all three lists,
19497
+ * prioritized as added -> changed -> removed (newer-state first).
19498
+ * When more changes exist than fit, the next call with the returned
19499
+ * revision will pick up the rest because each entry's
19500
+ * last_modified_revision is unchanged by truncation.
19501
+ */
19502
+ async getSync(opts) {
19503
+ await this.hydrate();
19504
+ await this.expireStale();
19505
+ const sinceRevision = opts?.sinceRevision ?? 0;
19506
+ const cap = Math.min(
19507
+ opts?.limit ?? DEFAULT_LIST_PAGE_SIZE,
19508
+ this.maxListLimit
19509
+ );
19510
+ const added = [];
19511
+ const changed = [];
19512
+ for (const entry of this.entries.values()) {
19513
+ const lastMod = entry.last_modified_revision ?? 0;
19514
+ if (lastMod <= sinceRevision) continue;
19515
+ const createdRev = entry.created_at_revision ?? 0;
19516
+ if (createdRev > sinceRevision) {
19517
+ added.push(entry);
19518
+ } else {
19519
+ changed.push(entry);
19520
+ }
19521
+ }
19522
+ const removed = [];
19523
+ for (const [id, rev] of this.removedTombstones) {
19524
+ if (rev > sinceRevision) removed.push(id);
19525
+ }
19526
+ added.sort(
19527
+ (a, b) => (a.last_modified_revision ?? 0) - (b.last_modified_revision ?? 0)
19528
+ );
19529
+ changed.sort(
19530
+ (a, b) => (a.last_modified_revision ?? 0) - (b.last_modified_revision ?? 0)
19531
+ );
19532
+ let remaining = cap;
19533
+ const addedOut = added.slice(0, Math.max(0, remaining));
19534
+ remaining -= addedOut.length;
19535
+ const changedOut = changed.slice(0, Math.max(0, remaining));
19536
+ remaining -= changedOut.length;
19537
+ const removedOut = removed.slice(0, Math.max(0, remaining));
19538
+ return {
19539
+ revision: this.currentRevision,
19540
+ added: addedOut,
19541
+ changed: changedOut,
19542
+ removed: removedOut
19543
+ };
19544
+ }
19545
+ /**
19546
+ * Delete an entry. Drops the in-memory record, the persisted bundle,
19547
+ * and the at-rest payload (if a payload store is wired). Records a
19548
+ * tombstone with the new revision so sync-API consumers see a
19549
+ * `removed` delta. Returns true when an entry was deleted, false on
19550
+ * unknown id. v1.3 Upsilon-4. Reserved for v1.4+ retention housekeeping;
19551
+ * Upsilon-4 ships the surface so mobile sync-API tests can exercise the
19552
+ * removal path.
19553
+ */
19554
+ async deleteEntry(aggregatorId) {
19555
+ await this.hydrate();
19556
+ const entry = this.entries.get(aggregatorId);
19557
+ if (!entry) return false;
19558
+ const dedupKey = `${entry.source_harness}|${entry.source_agent_id}|${entry.audit_log_entry_id}`;
19559
+ this.entries.delete(aggregatorId);
19560
+ this.dedupIndex.delete(dedupKey);
19561
+ this.fullPayloads.delete(aggregatorId);
19562
+ for (const [corr, id] of this.correlationIndex) {
19563
+ if (id === aggregatorId) this.correlationIndex.delete(corr);
19564
+ }
19565
+ try {
19566
+ await this.storage.delete(APPROVAL_AGGREGATOR_NAMESPACE, aggregatorId);
19567
+ } catch {
19568
+ }
19569
+ if (this.payloadStore) {
19570
+ try {
19571
+ await this.payloadStore.deletePayload(aggregatorId);
19572
+ } catch {
19573
+ }
19574
+ }
19575
+ const revision = this.nextRevision();
19576
+ this.removedTombstones.set(aggregatorId, revision);
19577
+ this.emit({ type: "removed", entry: { ...entry } });
19578
+ return true;
19579
+ }
19580
+ nextRevision() {
19581
+ this.currentRevision += 1;
19582
+ return this.currentRevision;
19583
+ }
19444
19584
  /**
19445
19585
  * Ingest a gate event. Returns the aggregator entry on first sight,
19446
19586
  * `null` when deduped. Resolution events update the existing record;
@@ -19651,6 +19791,7 @@ var ApprovalAggregator = class {
19651
19791
  entry.status = decision;
19652
19792
  entry.resolved_at = this.now().toISOString();
19653
19793
  entry.resolved_by = operatorId;
19794
+ entry.last_modified_revision = this.nextRevision();
19654
19795
  await this.persist(entry);
19655
19796
  this.auditLog.append(
19656
19797
  "l2",
@@ -19702,6 +19843,7 @@ var ApprovalAggregator = class {
19702
19843
  const expires = new Date(now.getTime() + this.pendingTtlMs);
19703
19844
  const hubInboxId = this.resolveHubInboxItemId(event);
19704
19845
  const enforcementChain = this.resolveEnforcementChain(event);
19846
+ const revision = this.nextRevision();
19705
19847
  const entry = {
19706
19848
  aggregator_id: id,
19707
19849
  source_harness: ctx.source_harness,
@@ -19713,6 +19855,8 @@ var ApprovalAggregator = class {
19713
19855
  status: "pending",
19714
19856
  created_at: now.toISOString(),
19715
19857
  expires_at: expires.toISOString(),
19858
+ created_at_revision: revision,
19859
+ last_modified_revision: revision,
19716
19860
  ...hubInboxId !== void 0 ? { hub_inbox_item_id: hubInboxId } : {},
19717
19861
  ...enforcementChain.length > 0 ? { enforcement_chain: enforcementChain } : {}
19718
19862
  };
@@ -19755,6 +19899,7 @@ var ApprovalAggregator = class {
19755
19899
  entry.status = status;
19756
19900
  entry.resolved_at = event.resolution.decided_at;
19757
19901
  entry.resolved_by = event.resolution.decided_by;
19902
+ entry.last_modified_revision = this.nextRevision();
19758
19903
  await this.persist(entry);
19759
19904
  this.auditLog.append(
19760
19905
  "l2",
@@ -19817,6 +19962,7 @@ var ApprovalAggregator = class {
19817
19962
  entry.status = "expired";
19818
19963
  entry.resolved_at = this.now().toISOString();
19819
19964
  entry.resolved_by = "system_ttl";
19965
+ entry.last_modified_revision = this.nextRevision();
19820
19966
  await this.persist(entry);
19821
19967
  this.auditLog.append(
19822
19968
  "l2",
@@ -19863,6 +20009,10 @@ var ApprovalAggregator = class {
19863
20009
  this.entries.set(entry.aggregator_id, entry);
19864
20010
  const dedupKey = `${entry.source_harness}|${entry.source_agent_id}|${entry.audit_log_entry_id}`;
19865
20011
  this.dedupIndex.set(dedupKey, entry.aggregator_id);
20012
+ const lastMod = entry.last_modified_revision ?? 0;
20013
+ if (lastMod > this.currentRevision) {
20014
+ this.currentRevision = lastMod;
20015
+ }
19866
20016
  } catch {
19867
20017
  }
19868
20018
  }
@@ -32313,6 +32463,13 @@ init_encoding();
32313
32463
  // src/chat/operator-chat-audit-events.ts
32314
32464
  var OPERATOR_CHAT_OPS = {
32315
32465
  CONCIERGE_CHAT: "operator_concierge_chat",
32466
+ /**
32467
+ * Click-to-inspect panel opened on an agent row. Repurposed from the
32468
+ * direct-agent session-open audit event in the v1.2 reshape; the click
32469
+ * affordance now opens an inspect/approve panel (recent activity +
32470
+ * pending approvals + policy summary) instead of a chat session.
32471
+ */
32472
+ AGENT_INSPECT_PANEL_OPENED: "agent_inspect_panel_opened",
32316
32473
  /**
32317
32474
  * Operator viewed concierge thread history (WP-V1.3-9 Tau-1). Emitted
32318
32475
  * when the operator hits the list-threads or read-thread route. Body
@@ -32481,9 +32638,10 @@ function isTrivialQuery(query) {
32481
32638
  if (norm.length < 8) return true;
32482
32639
  return TRIVIAL_GREETINGS.has(norm);
32483
32640
  }
32484
- function classifyQuery(query) {
32641
+ function classifyQuery(query, parsedGrammar) {
32485
32642
  const normalized = query.toLowerCase();
32486
32643
  const matches = [];
32644
+ const grammarAgent = parsedGrammar && parsedGrammar.agent_names.length > 0 ? parsedGrammar.agent_names[0] ?? null : null;
32487
32645
  for (const spec of CATEGORY_KEYWORDS) {
32488
32646
  const matchedPhrases = [];
32489
32647
  for (const pattern of spec.patterns) {
@@ -32495,11 +32653,14 @@ function classifyQuery(query) {
32495
32653
  }
32496
32654
  if (matchedPhrases.length === 0) continue;
32497
32655
  const confidence = Math.min(1, 0.4 + 0.3 * matchedPhrases.length);
32656
+ const wantsAgentHint = spec.category === "agent_state" || spec.category === "agent_activity";
32657
+ const agent_name_hint = wantsAgentHint ? grammarAgent ?? extractAgentNameHint(query) : null;
32498
32658
  matches.push({
32499
32659
  category: spec.category,
32500
32660
  confidence,
32501
32661
  matched_keywords: matchedPhrases,
32502
- agent_name_hint: spec.category === "agent_state" || spec.category === "agent_activity" ? extractAgentNameHint(query) : null
32662
+ agent_name_hint,
32663
+ ...parsedGrammar !== void 0 ? { query_grammar: parsedGrammar } : {}
32503
32664
  });
32504
32665
  }
32505
32666
  matches.sort((a, b) => {
@@ -32508,6 +32669,25 @@ function classifyQuery(query) {
32508
32669
  });
32509
32670
  return matches;
32510
32671
  }
32672
+ function fetcherHintsFromGrammar(parsed) {
32673
+ if (!parsed) return void 0;
32674
+ const hasTime = parsed.time_range !== null;
32675
+ const hasAgents = parsed.agent_names.length > 0;
32676
+ const hasEvents = parsed.event_types.length > 0;
32677
+ if (!hasTime && !hasAgents && !hasEvents) return void 0;
32678
+ const hints = {};
32679
+ if (parsed.time_range) {
32680
+ const range = parsed.time_range;
32681
+ hints.time_range = {
32682
+ start: range.start,
32683
+ end: range.end,
32684
+ ...range.relative_label !== void 0 ? { relative_label: range.relative_label } : {}
32685
+ };
32686
+ }
32687
+ if (hasAgents) hints.agent_names = parsed.agent_names;
32688
+ if (hasEvents) hints.event_types = parsed.event_types;
32689
+ return hints;
32690
+ }
32511
32691
  function approxTokenLen(text) {
32512
32692
  return Math.ceil(text.length / APPROX_CHARS_PER_TOKEN);
32513
32693
  }
@@ -32521,42 +32701,45 @@ var CATEGORY_LABELS = {
32521
32701
  recent_receipts: "Recent receipts",
32522
32702
  verascore_deltas: "Verascore deltas"
32523
32703
  };
32524
- async function runFetcher(match, fetchers) {
32704
+ async function runFetcher(match, fetchers, hints) {
32525
32705
  switch (match.category) {
32526
32706
  case "templates":
32527
- return fetchers.templates();
32707
+ return fetchers.templates(hints);
32528
32708
  case "agent_state":
32529
- return fetchers.agent_state(match.agent_name_hint);
32709
+ return fetchers.agent_state(match.agent_name_hint, hints);
32530
32710
  case "agent_activity":
32531
- return fetchers.agent_activity(match.agent_name_hint);
32711
+ return fetchers.agent_activity(match.agent_name_hint, hints);
32532
32712
  case "audit_log":
32533
- return fetchers.audit_log();
32713
+ return fetchers.audit_log(hints);
32534
32714
  case "sentinel_findings":
32535
- return fetchers.sentinel_findings();
32715
+ return fetchers.sentinel_findings(hints);
32536
32716
  case "anomaly_alerts":
32537
- return fetchers.anomaly_alerts();
32717
+ return fetchers.anomaly_alerts(hints);
32538
32718
  case "recent_receipts":
32539
- return fetchers.recent_receipts();
32719
+ return fetchers.recent_receipts(hints);
32540
32720
  case "verascore_deltas":
32541
- return fetchers.verascore_deltas();
32721
+ return fetchers.verascore_deltas(hints);
32542
32722
  }
32543
32723
  }
32544
- function trivialMatch(category) {
32724
+ function trivialMatch(category, parsedGrammar) {
32545
32725
  return {
32546
32726
  category,
32547
32727
  confidence: 0.5,
32548
32728
  matched_keywords: ["llm-assist"],
32549
- agent_name_hint: null
32729
+ agent_name_hint: parsedGrammar && parsedGrammar.agent_names.length > 0 ? parsedGrammar.agent_names[0] ?? null : null,
32730
+ ...parsedGrammar !== void 0 ? { query_grammar: parsedGrammar } : {}
32550
32731
  };
32551
32732
  }
32552
32733
  async function foldContext(query, fetchers, opts) {
32553
32734
  const budget = opts?.maxTokens ?? DEFAULT_DYNAMIC_CONTEXT_TOKEN_BUDGET;
32554
- let matches = classifyQuery(query);
32735
+ const parsed = opts?.parsed ?? null;
32736
+ const hints = fetcherHintsFromGrammar(parsed);
32737
+ let matches = classifyQuery(query, parsed);
32555
32738
  if (matches.length === 0 && !isTrivialQuery(query) && opts?.llmAssistClassify) {
32556
32739
  try {
32557
32740
  const picked = await opts.llmAssistClassify(query, CONTEXT_CATEGORIES);
32558
32741
  if (picked !== "none" && CONTEXT_CATEGORIES.includes(picked)) {
32559
- matches = [trivialMatch(picked)];
32742
+ matches = [trivialMatch(picked, parsed)];
32560
32743
  }
32561
32744
  } catch {
32562
32745
  }
@@ -32567,7 +32750,7 @@ async function foldContext(query, fetchers, opts) {
32567
32750
  const attempts = [];
32568
32751
  for (const match of matches) {
32569
32752
  try {
32570
- const text = await runFetcher(match, fetchers);
32753
+ const text = await runFetcher(match, fetchers, hints);
32571
32754
  const trimmed = text.trim();
32572
32755
  if (trimmed.length > 0) {
32573
32756
  attempts.push({ category: match.category, text: trimmed });
@@ -32609,6 +32792,520 @@ ${blocks.join("\n\n")}`;
32609
32792
  };
32610
32793
  }
32611
32794
 
32795
+ // src/composition/constants.ts
32796
+ var COMPOSITION_EVENT_TYPES = [
32797
+ "composition_receipt_packed",
32798
+ "composition_receipt_verified",
32799
+ "composition_mandate_verified",
32800
+ "composition_verascore_published",
32801
+ "composition_sidecar_spawned",
32802
+ "composition_sidecar_crashed",
32803
+ "composition_sidecar_recovered",
32804
+ "composition_degraded",
32805
+ "composition_recovered"
32806
+ ];
32807
+
32808
+ // src/chat/concierge-query-grammar.ts
32809
+ var CANONICAL_AUDIT_EVENT_CLASSES = [
32810
+ // Lifecycle / policy
32811
+ "policy_change",
32812
+ "approval_request",
32813
+ "audit_truncate",
32814
+ "lockdown",
32815
+ "unwrap",
32816
+ // Exit bundle (Tier 1)
32817
+ "exit_bundle_export",
32818
+ "exit_bundle_import_activate",
32819
+ "exit_bundle_rekey",
32820
+ // Cross-harness approval aggregator
32821
+ "cross_harness_approval_aggregated",
32822
+ "cross_harness_approval_resolved",
32823
+ "cross_harness_approval_deduped",
32824
+ "cross_harness_approval_payload_decrypted",
32825
+ "cross_harness_approval_audit_trail_viewed",
32826
+ "cross_harness_approval_replayed",
32827
+ // Composition (full set from constants.ts)
32828
+ ...COMPOSITION_EVENT_TYPES,
32829
+ // Operator chat / concierge (full set from OPERATOR_CHAT_OPS)
32830
+ OPERATOR_CHAT_OPS.CONCIERGE_CHAT,
32831
+ OPERATOR_CHAT_OPS.AGENT_INSPECT_PANEL_OPENED,
32832
+ OPERATOR_CHAT_OPS.CONCIERGE_HISTORY_READ,
32833
+ OPERATOR_CHAT_OPS.CONCIERGE_THREAD_DELETED,
32834
+ OPERATOR_CHAT_OPS.CONCIERGE_MEMORY_READ_FAILED,
32835
+ OPERATOR_CHAT_OPS.CONCIERGE_CONTEXT_FETCHER_FAILED,
32836
+ // Bridge / commitment
32837
+ "bridge_commit",
32838
+ "bridge_verify",
32839
+ "bridge_attest",
32840
+ "proof_commitment",
32841
+ "proof_reveal",
32842
+ // Reputation
32843
+ "reputation_export",
32844
+ "reputation_import",
32845
+ "reputation_publish",
32846
+ "reputation_record",
32847
+ "reputation_query"
32848
+ ];
32849
+ var EVENT_SYNONYMS = [
32850
+ { phrase: "approvals", canonical: ["approval_request", "cross_harness_approval_aggregated", "cross_harness_approval_resolved"] },
32851
+ { phrase: "approval", canonical: ["approval_request"] },
32852
+ { phrase: "policy changes", canonical: ["policy_change"] },
32853
+ { phrase: "policy change", canonical: ["policy_change"] },
32854
+ { phrase: "policy edits", canonical: ["policy_change"] },
32855
+ { phrase: "lockdowns", canonical: ["lockdown"] },
32856
+ { phrase: "exit bundles", canonical: ["exit_bundle_export", "exit_bundle_import_activate"] },
32857
+ { phrase: "exit bundle", canonical: ["exit_bundle_export"] },
32858
+ { phrase: "audit truncations", canonical: ["audit_truncate"] },
32859
+ { phrase: "audit truncation", canonical: ["audit_truncate"] },
32860
+ { phrase: "compositions", canonical: ["composition_receipt_packed", "composition_receipt_verified"] },
32861
+ { phrase: "receipts", canonical: ["composition_receipt_packed", "composition_receipt_verified"] },
32862
+ { phrase: "receipt verifications", canonical: ["composition_receipt_verified"] },
32863
+ { phrase: "concierge chats", canonical: [OPERATOR_CHAT_OPS.CONCIERGE_CHAT] },
32864
+ { phrase: "cross harness approvals", canonical: ["cross_harness_approval_aggregated", "cross_harness_approval_resolved"] }
32865
+ ];
32866
+ var MS_PER_HOUR = 60 * 60 * 1e3;
32867
+ var MS_PER_DAY = 24 * MS_PER_HOUR;
32868
+ var NUMBER_WORDS = {
32869
+ a: 1,
32870
+ an: 1,
32871
+ one: 1,
32872
+ two: 2,
32873
+ three: 3,
32874
+ four: 4,
32875
+ five: 5,
32876
+ six: 6,
32877
+ seven: 7,
32878
+ eight: 8,
32879
+ nine: 9,
32880
+ ten: 10,
32881
+ twelve: 12,
32882
+ twentyfour: 24
32883
+ };
32884
+ function resolveTimeRange(query, now) {
32885
+ const normalized = query.trim();
32886
+ const lower = normalized.toLowerCase();
32887
+ const fromTo = lower.match(
32888
+ /\b(?:from|between)\s+(.+?)\s+(?:to|and|-|until)\s+([\w:.\-+t /]+)/i
32889
+ );
32890
+ if (fromTo) {
32891
+ const aSlice = fromTo[1];
32892
+ const bSlice = fromTo[2];
32893
+ if (aSlice !== void 0 && bSlice !== void 0) {
32894
+ const a = parseInstant(aSlice, now);
32895
+ const b = parseInstant(bSlice, now);
32896
+ if (a && b) {
32897
+ const start = a.getTime() <= b.getTime() ? a : b;
32898
+ const end = a.getTime() <= b.getTime() ? b : a;
32899
+ return {
32900
+ range: { start, end },
32901
+ matchedSubstring: fromTo[0]
32902
+ };
32903
+ }
32904
+ }
32905
+ }
32906
+ const sinceMatch = lower.match(/\bsince\s+([\w:.\-+t /]+)/i);
32907
+ if (sinceMatch) {
32908
+ const slice = sinceMatch[1];
32909
+ if (slice !== void 0) {
32910
+ const start = parseInstant(slice, now);
32911
+ if (start) {
32912
+ return {
32913
+ range: { start, end: now },
32914
+ matchedSubstring: sinceMatch[0]
32915
+ };
32916
+ }
32917
+ }
32918
+ }
32919
+ if (/\byesterday\b/.test(lower)) {
32920
+ const startOfToday = startOfDay(now);
32921
+ const start = new Date(startOfToday.getTime() - MS_PER_DAY);
32922
+ const end = new Date(startOfToday.getTime() - 1);
32923
+ return {
32924
+ range: { start, end, relative_label: "yesterday" },
32925
+ matchedSubstring: "yesterday"
32926
+ };
32927
+ }
32928
+ if (/\btoday\b/.test(lower)) {
32929
+ return {
32930
+ range: {
32931
+ start: startOfDay(now),
32932
+ end: now,
32933
+ relative_label: "today"
32934
+ },
32935
+ matchedSubstring: "today"
32936
+ };
32937
+ }
32938
+ const compactHours = lower.match(/\blast\s+(\d+)\s*h\b/i);
32939
+ if (compactHours) {
32940
+ const tok = compactHours[1];
32941
+ if (tok !== void 0) {
32942
+ const n = Number.parseInt(tok, 10);
32943
+ if (Number.isFinite(n) && n > 0) {
32944
+ const start = new Date(now.getTime() - n * MS_PER_HOUR);
32945
+ return {
32946
+ range: { start, end: now, relative_label: `last ${n}h` },
32947
+ matchedSubstring: compactHours[0]
32948
+ };
32949
+ }
32950
+ }
32951
+ }
32952
+ const hoursMatch = lower.match(
32953
+ /\b(?:past|last)\s+([\w]+|\d+)\s*(?:hr\b|hrs\b|hour|hours)/i
32954
+ );
32955
+ if (hoursMatch) {
32956
+ const tok = hoursMatch[1];
32957
+ if (tok !== void 0) {
32958
+ const n = parseCount(tok);
32959
+ if (n !== null && n > 0) {
32960
+ const start = new Date(now.getTime() - n * MS_PER_HOUR);
32961
+ return {
32962
+ range: { start, end: now, relative_label: `past ${n} hour${n === 1 ? "" : "s"}` },
32963
+ matchedSubstring: hoursMatch[0]
32964
+ };
32965
+ }
32966
+ }
32967
+ }
32968
+ if (/\b(?:past|last)\s+hour\b/.test(lower)) {
32969
+ const start = new Date(now.getTime() - MS_PER_HOUR);
32970
+ return {
32971
+ range: { start, end: now, relative_label: "past hour" },
32972
+ matchedSubstring: lower.match(/\b(?:past|last)\s+hour\b/i)[0]
32973
+ };
32974
+ }
32975
+ const daysMatch = lower.match(
32976
+ /\b(?:past|last)\s+([\w]+|\d+)\s*(?:d\b|day|days)/i
32977
+ );
32978
+ if (daysMatch) {
32979
+ const tok = daysMatch[1];
32980
+ if (tok !== void 0) {
32981
+ const n = parseCount(tok);
32982
+ if (n !== null && n > 0) {
32983
+ const start = new Date(now.getTime() - n * MS_PER_DAY);
32984
+ return {
32985
+ range: { start, end: now, relative_label: `past ${n} day${n === 1 ? "" : "s"}` },
32986
+ matchedSubstring: daysMatch[0]
32987
+ };
32988
+ }
32989
+ }
32990
+ }
32991
+ if (/\b(?:past|last)\s+day\b/.test(lower)) {
32992
+ const start = new Date(now.getTime() - MS_PER_DAY);
32993
+ return {
32994
+ range: { start, end: now, relative_label: "past day" },
32995
+ matchedSubstring: lower.match(/\b(?:past|last)\s+day\b/i)[0]
32996
+ };
32997
+ }
32998
+ if (/\bthis\s+week\b/.test(lower)) {
32999
+ const start = startOfWeek(now);
33000
+ return {
33001
+ range: { start, end: now, relative_label: "this week" },
33002
+ matchedSubstring: lower.match(/\bthis\s+week\b/i)[0]
33003
+ };
33004
+ }
33005
+ if (/\b(?:past|last)\s+week\b/.test(lower)) {
33006
+ const start = new Date(now.getTime() - 7 * MS_PER_DAY);
33007
+ return {
33008
+ range: { start, end: now, relative_label: "past week" },
33009
+ matchedSubstring: lower.match(/\b(?:past|last)\s+week\b/i)[0]
33010
+ };
33011
+ }
33012
+ const isoMatch = normalized.match(
33013
+ /\b(\d{4}-\d{2}-\d{2}(?:[T ]\d{2}:\d{2}(?::\d{2})?(?:Z|[+-]\d{2}:\d{2})?)?)\b/
33014
+ );
33015
+ if (isoMatch) {
33016
+ const tok = isoMatch[1];
33017
+ if (tok !== void 0) {
33018
+ const parsed = parseInstant(tok, now);
33019
+ if (parsed) {
33020
+ const isDateOnly = /^\d{4}-\d{2}-\d{2}$/.test(tok);
33021
+ if (isDateOnly) {
33022
+ return {
33023
+ range: {
33024
+ start: parsed,
33025
+ end: new Date(parsed.getTime() + MS_PER_DAY - 1)
33026
+ },
33027
+ matchedSubstring: tok
33028
+ };
33029
+ }
33030
+ return {
33031
+ range: {
33032
+ start: new Date(parsed.getTime() - 30 * 60 * 1e3),
33033
+ end: new Date(parsed.getTime() + 30 * 60 * 1e3)
33034
+ },
33035
+ matchedSubstring: tok
33036
+ };
33037
+ }
33038
+ }
33039
+ }
33040
+ return null;
33041
+ }
33042
+ function parseInstant(token, now) {
33043
+ const trimmed = token.trim().replace(/[,.!?;]+$/g, "");
33044
+ if (!trimmed) return null;
33045
+ const lower = trimmed.toLowerCase();
33046
+ if (lower === "now") return now;
33047
+ if (lower === "today") return startOfDay(now);
33048
+ if (lower === "yesterday") {
33049
+ return new Date(startOfDay(now).getTime() - MS_PER_DAY);
33050
+ }
33051
+ const isoLike = trimmed.replace(" ", "T");
33052
+ const parsed = new Date(isoLike);
33053
+ if (!Number.isNaN(parsed.getTime())) return parsed;
33054
+ return null;
33055
+ }
33056
+ function parseCount(token) {
33057
+ const lower = token.toLowerCase();
33058
+ if (/^\d+$/.test(lower)) {
33059
+ const n = Number.parseInt(lower, 10);
33060
+ return Number.isFinite(n) ? n : null;
33061
+ }
33062
+ return NUMBER_WORDS[lower] ?? null;
33063
+ }
33064
+ function startOfDay(d) {
33065
+ const out = new Date(d);
33066
+ out.setHours(0, 0, 0, 0);
33067
+ return out;
33068
+ }
33069
+ function startOfWeek(d) {
33070
+ const out = startOfDay(d);
33071
+ const dayOfWeek = out.getDay();
33072
+ const offsetToMonday = (dayOfWeek + 6) % 7;
33073
+ out.setDate(out.getDate() - offsetToMonday);
33074
+ return out;
33075
+ }
33076
+ function listFromRegistry(registry) {
33077
+ if (!registry) return [];
33078
+ if (Array.isArray(registry)) return registry;
33079
+ if (typeof registry.list === "function") {
33080
+ return registry.list();
33081
+ }
33082
+ return [];
33083
+ }
33084
+ function extractAgentNames(query, registry) {
33085
+ const records = listFromRegistry(registry);
33086
+ if (records.length === 0) return { matched: [], flagged: false };
33087
+ const lowerQuery = query.toLowerCase();
33088
+ const compactQuery = lowerQuery.replace(/[\s_-]+/g, "");
33089
+ const matched = [];
33090
+ const seen = /* @__PURE__ */ new Set();
33091
+ for (const rec of records) {
33092
+ const id = rec.agent_id;
33093
+ if (!id || seen.has(id)) continue;
33094
+ const idLower = id.toLowerCase();
33095
+ if (idLower.length < 3) continue;
33096
+ const idCompact = idLower.replace(/[\s_-]+/g, "");
33097
+ const wordRe = new RegExp(
33098
+ `\\b${idLower.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`,
33099
+ "i"
33100
+ );
33101
+ if (wordRe.test(query)) {
33102
+ matched.push(id);
33103
+ seen.add(id);
33104
+ continue;
33105
+ }
33106
+ if (idCompact.length >= 4 && compactQuery.includes(idCompact)) {
33107
+ matched.push(id);
33108
+ seen.add(id);
33109
+ }
33110
+ }
33111
+ const agentMention = lowerQuery.match(/\bagent\s+([a-z0-9_-]{3,40})/i);
33112
+ const flagged = matched.length === 0 && agentMention !== null && agentMention[1] !== void 0 && !records.some((r) => r.agent_id.toLowerCase() === agentMention[1]?.toLowerCase());
33113
+ return { matched, flagged };
33114
+ }
33115
+ function escapeRegex(s) {
33116
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
33117
+ }
33118
+ function extractEventTypes(query, enumValues) {
33119
+ const lower = query.toLowerCase();
33120
+ const matched = [];
33121
+ const seen = /* @__PURE__ */ new Set();
33122
+ for (const ev of enumValues) {
33123
+ if (seen.has(ev)) continue;
33124
+ const re = new RegExp(`\\b${escapeRegex(ev)}\\b`, "i");
33125
+ if (re.test(query)) {
33126
+ matched.push(ev);
33127
+ seen.add(ev);
33128
+ }
33129
+ }
33130
+ for (const syn of EVENT_SYNONYMS) {
33131
+ const re = new RegExp(
33132
+ `\\b${syn.phrase.split(/\s+/).map(escapeRegex).join("\\s+")}\\b`,
33133
+ "i"
33134
+ );
33135
+ if (re.test(query)) {
33136
+ for (const c of syn.canonical) {
33137
+ if (seen.has(c)) continue;
33138
+ if (!enumValues.includes(c)) continue;
33139
+ matched.push(c);
33140
+ seen.add(c);
33141
+ }
33142
+ }
33143
+ }
33144
+ const globMatches = lower.match(/\b([a-z_]+)_\*/g) ?? [];
33145
+ for (const glob of globMatches) {
33146
+ const prefix = glob.slice(0, -2);
33147
+ for (const ev of enumValues) {
33148
+ if (seen.has(ev)) continue;
33149
+ if (ev.startsWith(prefix)) {
33150
+ matched.push(ev);
33151
+ seen.add(ev);
33152
+ }
33153
+ }
33154
+ }
33155
+ const eventNounMention = /\b(?:event|events|class|classes)\b/i.test(query) && matched.length === 0;
33156
+ return { matched, flagged: eventNounMention };
33157
+ }
33158
+ function deriveIntentPhrase(query, stripTokens) {
33159
+ let out = query;
33160
+ for (const tok of stripTokens) {
33161
+ if (!tok) continue;
33162
+ const re = new RegExp(escapeRegex(tok), "gi");
33163
+ out = out.replace(re, " ");
33164
+ }
33165
+ return out.replace(/\s+/g, " ").trim();
33166
+ }
33167
+ function computeConfidence(parsed) {
33168
+ const dims = [
33169
+ { present: parsed.hasTimeMention, resolved: parsed.timeResolved },
33170
+ { present: parsed.hasAgentMention, resolved: parsed.agentResolved },
33171
+ { present: parsed.hasEventMention, resolved: parsed.eventResolved }
33172
+ ];
33173
+ const present = dims.filter((d) => d.present);
33174
+ let base;
33175
+ if (present.length === 0) {
33176
+ base = parsed.intentEmpty ? 0 : 0.3;
33177
+ } else {
33178
+ const resolved = present.filter((d) => d.resolved).length;
33179
+ base = resolved / present.length;
33180
+ }
33181
+ const adjusted = base - 0.15 * parsed.ambiguityCount;
33182
+ if (adjusted < 0) return 0;
33183
+ if (adjusted > 1) return 1;
33184
+ return adjusted;
33185
+ }
33186
+ var TIME_MENTION_PROBE = /\b(yesterday|today|now|past|last|this\s+week|this\s+month|since|from|between|\d{4}-\d{2}-\d{2})\b/i;
33187
+ var AGENT_MENTION_PROBE = /\bagent[s]?\b/i;
33188
+ var EVENT_MENTION_PROBE = /\b(event|events|class|classes|approvals?|policy)\b/i;
33189
+ function parseQuery(query, opts) {
33190
+ const now = opts?.now ?? /* @__PURE__ */ new Date();
33191
+ const enumValues = opts?.eventClassEnum ?? CANONICAL_AUDIT_EVENT_CLASSES;
33192
+ const original = query ?? "";
33193
+ const trimmed = original.trim();
33194
+ if (trimmed.length === 0) {
33195
+ return {
33196
+ time_range: null,
33197
+ agent_names: [],
33198
+ event_types: [],
33199
+ intent_phrase: "",
33200
+ ambiguity_flags: ["no_signal_extracted"],
33201
+ parse_confidence: 0
33202
+ };
33203
+ }
33204
+ const ambiguity_flags = /* @__PURE__ */ new Set();
33205
+ const timeMatch = resolveTimeRange(trimmed, now);
33206
+ const hasTimeMention = TIME_MENTION_PROBE.test(trimmed);
33207
+ if (hasTimeMention && !timeMatch) {
33208
+ ambiguity_flags.add("unknown_time_token");
33209
+ }
33210
+ const agentResult = extractAgentNames(trimmed, opts?.registry);
33211
+ if (agentResult.flagged) {
33212
+ ambiguity_flags.add("unknown_agent_token");
33213
+ }
33214
+ const hasAgentMention = AGENT_MENTION_PROBE.test(trimmed);
33215
+ const eventResult = extractEventTypes(trimmed, enumValues);
33216
+ const hasEventMention = EVENT_MENTION_PROBE.test(trimmed);
33217
+ if (eventResult.flagged) {
33218
+ ambiguity_flags.add("unknown_event_token");
33219
+ }
33220
+ const stripTokens = [];
33221
+ if (timeMatch) stripTokens.push(timeMatch.matchedSubstring);
33222
+ for (const name of agentResult.matched) stripTokens.push(name);
33223
+ for (const ev of eventResult.matched) {
33224
+ if (trimmed.toLowerCase().includes(ev.toLowerCase())) {
33225
+ stripTokens.push(ev);
33226
+ }
33227
+ }
33228
+ const intent_phrase = deriveIntentPhrase(trimmed, stripTokens);
33229
+ const parse_confidence = computeConfidence({
33230
+ hasTimeMention,
33231
+ timeResolved: timeMatch !== null,
33232
+ hasAgentMention,
33233
+ agentResolved: agentResult.matched.length > 0,
33234
+ hasEventMention,
33235
+ eventResolved: eventResult.matched.length > 0,
33236
+ intentEmpty: intent_phrase.length === 0,
33237
+ ambiguityCount: ambiguity_flags.size
33238
+ });
33239
+ if (timeMatch === null && agentResult.matched.length === 0 && eventResult.matched.length === 0 && intent_phrase.length === 0) {
33240
+ ambiguity_flags.add("no_signal_extracted");
33241
+ }
33242
+ return {
33243
+ time_range: timeMatch ? timeMatch.range : null,
33244
+ agent_names: agentResult.matched,
33245
+ event_types: eventResult.matched,
33246
+ intent_phrase,
33247
+ ambiguity_flags: Array.from(ambiguity_flags),
33248
+ parse_confidence
33249
+ };
33250
+ }
33251
+ var LLM_ASSIST_THRESHOLD = 0.5;
33252
+ function isLowConfidence(parsed) {
33253
+ return parsed.parse_confidence < LLM_ASSIST_THRESHOLD;
33254
+ }
33255
+ async function parseQueryWithLlmAssist(query, llmAssist, opts) {
33256
+ const parsed = parseQuery(query, opts);
33257
+ if (!llmAssist || !isLowConfidence(parsed)) return parsed;
33258
+ let completion;
33259
+ try {
33260
+ completion = await llmAssist(query, parsed);
33261
+ } catch {
33262
+ return parsed;
33263
+ }
33264
+ if (!completion || typeof completion !== "object") return parsed;
33265
+ const merged = { ...parsed };
33266
+ if (parsed.time_range === null && completion.time_range) {
33267
+ merged.time_range = completion.time_range;
33268
+ }
33269
+ if (parsed.agent_names.length === 0 && Array.isArray(completion.agent_names)) {
33270
+ merged.agent_names = completion.agent_names.filter(
33271
+ (s) => typeof s === "string" && s.length > 0
33272
+ );
33273
+ }
33274
+ if (parsed.event_types.length === 0 && Array.isArray(completion.event_types)) {
33275
+ const allowed = new Set(opts?.eventClassEnum ?? CANONICAL_AUDIT_EVENT_CLASSES);
33276
+ merged.event_types = completion.event_types.filter(
33277
+ (s) => typeof s === "string" && allowed.has(s)
33278
+ );
33279
+ }
33280
+ merged.parse_confidence = Math.max(
33281
+ parsed.parse_confidence,
33282
+ computeConfidence({
33283
+ hasTimeMention: TIME_MENTION_PROBE.test(query),
33284
+ timeResolved: merged.time_range !== null,
33285
+ hasAgentMention: AGENT_MENTION_PROBE.test(query),
33286
+ agentResolved: merged.agent_names.length > 0,
33287
+ hasEventMention: EVENT_MENTION_PROBE.test(query),
33288
+ eventResolved: merged.event_types.length > 0,
33289
+ intentEmpty: merged.intent_phrase.length === 0,
33290
+ ambiguityCount: merged.ambiguity_flags.length
33291
+ })
33292
+ );
33293
+ return merged;
33294
+ }
33295
+ function auditSafeSummary(parsed) {
33296
+ return {
33297
+ time_range: parsed.time_range ? {
33298
+ start_iso: parsed.time_range.start.toISOString(),
33299
+ end_iso: parsed.time_range.end.toISOString(),
33300
+ ...parsed.time_range.relative_label !== void 0 ? { relative_label: parsed.time_range.relative_label } : {}
33301
+ } : null,
33302
+ agent_names: [...parsed.agent_names],
33303
+ event_types: [...parsed.event_types],
33304
+ ambiguity_flags: [...parsed.ambiguity_flags],
33305
+ parse_confidence: parsed.parse_confidence
33306
+ };
33307
+ }
33308
+
32612
33309
  // src/chat/operator-chat-service.ts
32613
33310
  var DEFAULT_CONCIERGE_MAX_TOKENS = 512;
32614
33311
  var DEFAULT_CONCIERGE_HISTORY_WINDOW_TURNS = 10;
@@ -32663,6 +33360,8 @@ var OperatorChatService = class {
32663
33360
  contextFetchers;
32664
33361
  contextLlmAssist;
32665
33362
  dynamicContextBudget;
33363
+ agentRegistry;
33364
+ grammarLlmAssist;
32666
33365
  /**
32667
33366
  * In-memory thread_id assigned to the active concierge session.
32668
33367
  * The first sendConcierge call after construction allocates a fresh
@@ -32701,6 +33400,12 @@ var OperatorChatService = class {
32701
33400
  this.contextLlmAssist = deps.conciergeContextLlmAssist;
32702
33401
  }
32703
33402
  this.dynamicContextBudget = deps.conciergeDynamicContextBudget !== void 0 && deps.conciergeDynamicContextBudget > 0 ? deps.conciergeDynamicContextBudget : DEFAULT_CONCIERGE_DYNAMIC_CONTEXT_BUDGET;
33403
+ if (deps.conciergeAgentRegistry) {
33404
+ this.agentRegistry = deps.conciergeAgentRegistry;
33405
+ }
33406
+ if (deps.conciergeGrammarLlmAssist) {
33407
+ this.grammarLlmAssist = deps.conciergeGrammarLlmAssist;
33408
+ }
32704
33409
  }
32705
33410
  // ── Concierge ─────────────────────────────────────────────────────────
32706
33411
  /**
@@ -32759,6 +33464,7 @@ var OperatorChatService = class {
32759
33464
  await this.memory.appendTurn(threadId, "user", trimmed).catch(() => {
32760
33465
  });
32761
33466
  }
33467
+ const parsedGrammar = await this.runGrammarParse(filterResult.filtered);
32762
33468
  const start = Date.now();
32763
33469
  let conciergeBody;
32764
33470
  let servedBy = "disabled";
@@ -32777,7 +33483,8 @@ var OperatorChatService = class {
32777
33483
  outcome = "substrate_disabled";
32778
33484
  } else {
32779
33485
  const dynamicResult = await this.runDynamicContextFold(
32780
- filterResult.filtered
33486
+ filterResult.filtered,
33487
+ parsedGrammar
32781
33488
  );
32782
33489
  dynamicCategoriesIncluded = dynamicResult.categoriesIncluded;
32783
33490
  const context = await this.assembleConciergeContext(
@@ -32848,7 +33555,8 @@ var OperatorChatService = class {
32848
33555
  ...this.memory ? {
32849
33556
  prior_turns_folded: memoryReadFailureReason === null ? priorTurns.length : 0
32850
33557
  } : {},
32851
- ...this.contextFetchers ? { dynamic_context_categories: [...dynamicCategoriesIncluded] } : {}
33558
+ ...this.contextFetchers ? { dynamic_context_categories: [...dynamicCategoriesIncluded] } : {},
33559
+ parsed_grammar: auditSafeSummary(parsedGrammar)
32852
33560
  };
32853
33561
  this.emit(OPERATOR_CHAT_OPS.CONCIERGE_CHAT, payload, outcome === "ok" ? "success" : "failure");
32854
33562
  return {
@@ -33063,8 +33771,12 @@ ${inbox}`
33063
33771
  * proceeds with no fold. Returns the rendered section + the list of
33064
33772
  * categories whose data made it into the section (used for the
33065
33773
  * round-trip audit emission).
33774
+ *
33775
+ * Tau-4: receives the pre-parsed `ParsedQuery` and forwards it as the
33776
+ * `parsed` opt to `foldContext`, so fetchers see the structured
33777
+ * `FetcherHints` derived from it.
33066
33778
  */
33067
- async runDynamicContextFold(query) {
33779
+ async runDynamicContextFold(query, parsedGrammar) {
33068
33780
  if (!this.contextFetchers) {
33069
33781
  return { section: "", categoriesIncluded: [] };
33070
33782
  }
@@ -33073,10 +33785,24 @@ ${inbox}`
33073
33785
  ...this.contextLlmAssist ? { llmAssistClassify: this.contextLlmAssist } : {},
33074
33786
  onFetcherFailure: (category, error) => {
33075
33787
  this.emitContextFetcherFailed(category, classifyFetcherError(error));
33076
- }
33788
+ },
33789
+ parsed: parsedGrammar
33077
33790
  });
33078
33791
  return result;
33079
33792
  }
33793
+ /**
33794
+ * WP-V1.3-9 Tau-4: parse the (PII-filtered) operator query into a
33795
+ * `ParsedQuery`. Routes through the LLM-assist completion hook when
33796
+ * configured and the rule-based parse is below
33797
+ * `LLM_ASSIST_THRESHOLD`. Always returns a parse object (never
33798
+ * throws) so the audit emission can carry the result unconditionally.
33799
+ */
33800
+ async runGrammarParse(query) {
33801
+ return parseQueryWithLlmAssist(query, this.grammarLlmAssist, {
33802
+ ...this.agentRegistry !== void 0 ? { registry: this.agentRegistry } : {},
33803
+ eventClassEnum: CANONICAL_AUDIT_EVENT_CLASSES
33804
+ });
33805
+ }
33080
33806
  /**
33081
33807
  * Emit the WP-V1.3-9 Tau-3 fetcher-failure audit event. Pulled out
33082
33808
  * of the fold path so the dynamic-context handler stays readable.