@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.cjs CHANGED
@@ -4922,7 +4922,8 @@ var SIGNATURE_SCHEME_V1 = "ed25519-v1";
4922
4922
  var RESERVED_EVENT_TYPE_PREFIXES = [
4923
4923
  "EXTENSION_",
4924
4924
  "cross_fortress_",
4925
- "multi_master_"
4925
+ "multi_master_",
4926
+ "cross_harness_approval_"
4926
4927
  ];
4927
4928
  function isReservedEventType(s) {
4928
4929
  return RESERVED_EVENT_TYPE_PREFIXES.some((p) => s.startsWith(p));
@@ -9102,9 +9103,9 @@ function fingerprintDID(did) {
9102
9103
  return `${raw.slice(0, 6)}\u2026${raw.slice(-6)}`;
9103
9104
  }
9104
9105
  function countInjectionsToday(audit) {
9105
- const startOfDay = /* @__PURE__ */ new Date();
9106
- startOfDay.setHours(0, 0, 0, 0);
9107
- const cutoff = startOfDay.getTime();
9106
+ const startOfDay2 = /* @__PURE__ */ new Date();
9107
+ startOfDay2.setHours(0, 0, 0, 0);
9108
+ const cutoff = startOfDay2.getTime();
9108
9109
  return audit.filter((e) => {
9109
9110
  const ts = new Date(e.timestamp).getTime();
9110
9111
  if (isNaN(ts) || ts < cutoff) return false;
@@ -9118,9 +9119,9 @@ var PROOF_CREATION_OPS = /* @__PURE__ */ new Set([
9118
9119
  "proof_commitment"
9119
9120
  ]);
9120
9121
  function countProofsToday(audit) {
9121
- const startOfDay = /* @__PURE__ */ new Date();
9122
- startOfDay.setHours(0, 0, 0, 0);
9123
- const cutoff = startOfDay.getTime();
9122
+ const startOfDay2 = /* @__PURE__ */ new Date();
9123
+ startOfDay2.setHours(0, 0, 0, 0);
9124
+ const cutoff = startOfDay2.getTime();
9124
9125
  return audit.filter((e) => {
9125
9126
  if (e.layer !== "l3") return false;
9126
9127
  if (!PROOF_CREATION_OPS.has(e.operation)) return false;
@@ -16319,6 +16320,24 @@ async function handleApprovalInboxRoute(deps, req, res) {
16319
16320
  await handleStream2(deps, res);
16320
16321
  return true;
16321
16322
  }
16323
+ if (method === "GET" && path === `${APPROVAL_INBOX_API_PREFIX}/revision`) {
16324
+ const revision = await deps.aggregator.getRevision();
16325
+ writeJSON4(res, 200, { ok: true, data: { revision } });
16326
+ return true;
16327
+ }
16328
+ if (method === "GET" && path === `${APPROVAL_INBOX_API_PREFIX}/sync`) {
16329
+ const sinceRaw = url.searchParams.get("since_revision");
16330
+ const sinceParsed = sinceRaw === null ? 0 : Number.parseInt(sinceRaw, 10);
16331
+ const sinceRevision = Number.isFinite(sinceParsed) && sinceParsed >= 0 ? sinceParsed : 0;
16332
+ const limit = parseLimit2(
16333
+ url.searchParams.get("limit"),
16334
+ APPROVAL_INBOX_DEFAULT_LIMIT,
16335
+ APPROVAL_INBOX_MAX_LIMIT
16336
+ );
16337
+ const delta = await deps.aggregator.getSync({ sinceRevision, limit });
16338
+ writeJSON4(res, 200, { ok: true, data: delta });
16339
+ return true;
16340
+ }
16322
16341
  if (method === "GET" && path === `${APPROVAL_INBOX_API_PREFIX}/history`) {
16323
16342
  const limit = parseLimit2(
16324
16343
  url.searchParams.get("limit"),
@@ -19414,6 +19433,20 @@ var ApprovalAggregator = class {
19414
19433
  hydrated = false;
19415
19434
  /** Active SSE listeners. */
19416
19435
  listeners = /* @__PURE__ */ new Set();
19436
+ /**
19437
+ * Monotonic revision counter, bumped on every mutation (ingest of new
19438
+ * entry, resolve, expire, delete). Hydrated from max(last_modified_revision)
19439
+ * across persisted entries on first read; in-memory after that. v1.3
19440
+ * Upsilon-4.
19441
+ */
19442
+ currentRevision = 0;
19443
+ /**
19444
+ * Removal tombstones: aggregator_id -> revision at removal. Used by the
19445
+ * sync API to surface "removed" entries to mobile consumers between
19446
+ * polls. In-memory only; server restart clears tombstones (mobile
19447
+ * bootstraps via `list()` on reconnect). v1.3 Upsilon-4.
19448
+ */
19449
+ removedTombstones = /* @__PURE__ */ new Map();
19417
19450
  constructor(deps) {
19418
19451
  this.storage = deps.storage;
19419
19452
  this.encryptionKey = derivePurposeKey(
@@ -19448,6 +19481,113 @@ var ApprovalAggregator = class {
19448
19481
  this.listeners.add(listener);
19449
19482
  return () => this.listeners.delete(listener);
19450
19483
  }
19484
+ /**
19485
+ * Current aggregator revision. v1.3 Upsilon-4. Mobile companions
19486
+ * poll the lightweight `/revision` route to detect that something
19487
+ * changed before fetching a full sync delta.
19488
+ */
19489
+ async getRevision() {
19490
+ await this.hydrate();
19491
+ return this.currentRevision;
19492
+ }
19493
+ /**
19494
+ * Compute a delta since `sinceRevision`. v1.3 Upsilon-4. Mobile
19495
+ * clients poll this for cheap state-sync. Behavior:
19496
+ * - `added`: entries whose `created_at_revision > sinceRevision`.
19497
+ * - `changed`: entries that existed at `sinceRevision` but had a
19498
+ * status transition (resolve, expire) since.
19499
+ * - `removed`: aggregator_ids deleted after `sinceRevision`.
19500
+ * - `revision`: current aggregator revision; pass this back as
19501
+ * `sinceRevision` on the next call.
19502
+ *
19503
+ * `limit` caps the total count returned across all three lists,
19504
+ * prioritized as added -> changed -> removed (newer-state first).
19505
+ * When more changes exist than fit, the next call with the returned
19506
+ * revision will pick up the rest because each entry's
19507
+ * last_modified_revision is unchanged by truncation.
19508
+ */
19509
+ async getSync(opts) {
19510
+ await this.hydrate();
19511
+ await this.expireStale();
19512
+ const sinceRevision = opts?.sinceRevision ?? 0;
19513
+ const cap = Math.min(
19514
+ opts?.limit ?? DEFAULT_LIST_PAGE_SIZE,
19515
+ this.maxListLimit
19516
+ );
19517
+ const added = [];
19518
+ const changed = [];
19519
+ for (const entry of this.entries.values()) {
19520
+ const lastMod = entry.last_modified_revision ?? 0;
19521
+ if (lastMod <= sinceRevision) continue;
19522
+ const createdRev = entry.created_at_revision ?? 0;
19523
+ if (createdRev > sinceRevision) {
19524
+ added.push(entry);
19525
+ } else {
19526
+ changed.push(entry);
19527
+ }
19528
+ }
19529
+ const removed = [];
19530
+ for (const [id, rev] of this.removedTombstones) {
19531
+ if (rev > sinceRevision) removed.push(id);
19532
+ }
19533
+ added.sort(
19534
+ (a, b) => (a.last_modified_revision ?? 0) - (b.last_modified_revision ?? 0)
19535
+ );
19536
+ changed.sort(
19537
+ (a, b) => (a.last_modified_revision ?? 0) - (b.last_modified_revision ?? 0)
19538
+ );
19539
+ let remaining = cap;
19540
+ const addedOut = added.slice(0, Math.max(0, remaining));
19541
+ remaining -= addedOut.length;
19542
+ const changedOut = changed.slice(0, Math.max(0, remaining));
19543
+ remaining -= changedOut.length;
19544
+ const removedOut = removed.slice(0, Math.max(0, remaining));
19545
+ return {
19546
+ revision: this.currentRevision,
19547
+ added: addedOut,
19548
+ changed: changedOut,
19549
+ removed: removedOut
19550
+ };
19551
+ }
19552
+ /**
19553
+ * Delete an entry. Drops the in-memory record, the persisted bundle,
19554
+ * and the at-rest payload (if a payload store is wired). Records a
19555
+ * tombstone with the new revision so sync-API consumers see a
19556
+ * `removed` delta. Returns true when an entry was deleted, false on
19557
+ * unknown id. v1.3 Upsilon-4. Reserved for v1.4+ retention housekeeping;
19558
+ * Upsilon-4 ships the surface so mobile sync-API tests can exercise the
19559
+ * removal path.
19560
+ */
19561
+ async deleteEntry(aggregatorId) {
19562
+ await this.hydrate();
19563
+ const entry = this.entries.get(aggregatorId);
19564
+ if (!entry) return false;
19565
+ const dedupKey = `${entry.source_harness}|${entry.source_agent_id}|${entry.audit_log_entry_id}`;
19566
+ this.entries.delete(aggregatorId);
19567
+ this.dedupIndex.delete(dedupKey);
19568
+ this.fullPayloads.delete(aggregatorId);
19569
+ for (const [corr, id] of this.correlationIndex) {
19570
+ if (id === aggregatorId) this.correlationIndex.delete(corr);
19571
+ }
19572
+ try {
19573
+ await this.storage.delete(APPROVAL_AGGREGATOR_NAMESPACE, aggregatorId);
19574
+ } catch {
19575
+ }
19576
+ if (this.payloadStore) {
19577
+ try {
19578
+ await this.payloadStore.deletePayload(aggregatorId);
19579
+ } catch {
19580
+ }
19581
+ }
19582
+ const revision = this.nextRevision();
19583
+ this.removedTombstones.set(aggregatorId, revision);
19584
+ this.emit({ type: "removed", entry: { ...entry } });
19585
+ return true;
19586
+ }
19587
+ nextRevision() {
19588
+ this.currentRevision += 1;
19589
+ return this.currentRevision;
19590
+ }
19451
19591
  /**
19452
19592
  * Ingest a gate event. Returns the aggregator entry on first sight,
19453
19593
  * `null` when deduped. Resolution events update the existing record;
@@ -19658,6 +19798,7 @@ var ApprovalAggregator = class {
19658
19798
  entry.status = decision;
19659
19799
  entry.resolved_at = this.now().toISOString();
19660
19800
  entry.resolved_by = operatorId;
19801
+ entry.last_modified_revision = this.nextRevision();
19661
19802
  await this.persist(entry);
19662
19803
  this.auditLog.append(
19663
19804
  "l2",
@@ -19709,6 +19850,7 @@ var ApprovalAggregator = class {
19709
19850
  const expires = new Date(now.getTime() + this.pendingTtlMs);
19710
19851
  const hubInboxId = this.resolveHubInboxItemId(event);
19711
19852
  const enforcementChain = this.resolveEnforcementChain(event);
19853
+ const revision = this.nextRevision();
19712
19854
  const entry = {
19713
19855
  aggregator_id: id,
19714
19856
  source_harness: ctx.source_harness,
@@ -19720,6 +19862,8 @@ var ApprovalAggregator = class {
19720
19862
  status: "pending",
19721
19863
  created_at: now.toISOString(),
19722
19864
  expires_at: expires.toISOString(),
19865
+ created_at_revision: revision,
19866
+ last_modified_revision: revision,
19723
19867
  ...hubInboxId !== void 0 ? { hub_inbox_item_id: hubInboxId } : {},
19724
19868
  ...enforcementChain.length > 0 ? { enforcement_chain: enforcementChain } : {}
19725
19869
  };
@@ -19762,6 +19906,7 @@ var ApprovalAggregator = class {
19762
19906
  entry.status = status;
19763
19907
  entry.resolved_at = event.resolution.decided_at;
19764
19908
  entry.resolved_by = event.resolution.decided_by;
19909
+ entry.last_modified_revision = this.nextRevision();
19765
19910
  await this.persist(entry);
19766
19911
  this.auditLog.append(
19767
19912
  "l2",
@@ -19824,6 +19969,7 @@ var ApprovalAggregator = class {
19824
19969
  entry.status = "expired";
19825
19970
  entry.resolved_at = this.now().toISOString();
19826
19971
  entry.resolved_by = "system_ttl";
19972
+ entry.last_modified_revision = this.nextRevision();
19827
19973
  await this.persist(entry);
19828
19974
  this.auditLog.append(
19829
19975
  "l2",
@@ -19870,6 +20016,10 @@ var ApprovalAggregator = class {
19870
20016
  this.entries.set(entry.aggregator_id, entry);
19871
20017
  const dedupKey = `${entry.source_harness}|${entry.source_agent_id}|${entry.audit_log_entry_id}`;
19872
20018
  this.dedupIndex.set(dedupKey, entry.aggregator_id);
20019
+ const lastMod = entry.last_modified_revision ?? 0;
20020
+ if (lastMod > this.currentRevision) {
20021
+ this.currentRevision = lastMod;
20022
+ }
19873
20023
  } catch {
19874
20024
  }
19875
20025
  }
@@ -32320,6 +32470,13 @@ init_encoding();
32320
32470
  // src/chat/operator-chat-audit-events.ts
32321
32471
  var OPERATOR_CHAT_OPS = {
32322
32472
  CONCIERGE_CHAT: "operator_concierge_chat",
32473
+ /**
32474
+ * Click-to-inspect panel opened on an agent row. Repurposed from the
32475
+ * direct-agent session-open audit event in the v1.2 reshape; the click
32476
+ * affordance now opens an inspect/approve panel (recent activity +
32477
+ * pending approvals + policy summary) instead of a chat session.
32478
+ */
32479
+ AGENT_INSPECT_PANEL_OPENED: "agent_inspect_panel_opened",
32323
32480
  /**
32324
32481
  * Operator viewed concierge thread history (WP-V1.3-9 Tau-1). Emitted
32325
32482
  * when the operator hits the list-threads or read-thread route. Body
@@ -32488,9 +32645,10 @@ function isTrivialQuery(query) {
32488
32645
  if (norm.length < 8) return true;
32489
32646
  return TRIVIAL_GREETINGS.has(norm);
32490
32647
  }
32491
- function classifyQuery(query) {
32648
+ function classifyQuery(query, parsedGrammar) {
32492
32649
  const normalized = query.toLowerCase();
32493
32650
  const matches = [];
32651
+ const grammarAgent = parsedGrammar && parsedGrammar.agent_names.length > 0 ? parsedGrammar.agent_names[0] ?? null : null;
32494
32652
  for (const spec of CATEGORY_KEYWORDS) {
32495
32653
  const matchedPhrases = [];
32496
32654
  for (const pattern of spec.patterns) {
@@ -32502,11 +32660,14 @@ function classifyQuery(query) {
32502
32660
  }
32503
32661
  if (matchedPhrases.length === 0) continue;
32504
32662
  const confidence = Math.min(1, 0.4 + 0.3 * matchedPhrases.length);
32663
+ const wantsAgentHint = spec.category === "agent_state" || spec.category === "agent_activity";
32664
+ const agent_name_hint = wantsAgentHint ? grammarAgent ?? extractAgentNameHint(query) : null;
32505
32665
  matches.push({
32506
32666
  category: spec.category,
32507
32667
  confidence,
32508
32668
  matched_keywords: matchedPhrases,
32509
- agent_name_hint: spec.category === "agent_state" || spec.category === "agent_activity" ? extractAgentNameHint(query) : null
32669
+ agent_name_hint,
32670
+ ...parsedGrammar !== void 0 ? { query_grammar: parsedGrammar } : {}
32510
32671
  });
32511
32672
  }
32512
32673
  matches.sort((a, b) => {
@@ -32515,6 +32676,25 @@ function classifyQuery(query) {
32515
32676
  });
32516
32677
  return matches;
32517
32678
  }
32679
+ function fetcherHintsFromGrammar(parsed) {
32680
+ if (!parsed) return void 0;
32681
+ const hasTime = parsed.time_range !== null;
32682
+ const hasAgents = parsed.agent_names.length > 0;
32683
+ const hasEvents = parsed.event_types.length > 0;
32684
+ if (!hasTime && !hasAgents && !hasEvents) return void 0;
32685
+ const hints = {};
32686
+ if (parsed.time_range) {
32687
+ const range = parsed.time_range;
32688
+ hints.time_range = {
32689
+ start: range.start,
32690
+ end: range.end,
32691
+ ...range.relative_label !== void 0 ? { relative_label: range.relative_label } : {}
32692
+ };
32693
+ }
32694
+ if (hasAgents) hints.agent_names = parsed.agent_names;
32695
+ if (hasEvents) hints.event_types = parsed.event_types;
32696
+ return hints;
32697
+ }
32518
32698
  function approxTokenLen(text) {
32519
32699
  return Math.ceil(text.length / APPROX_CHARS_PER_TOKEN);
32520
32700
  }
@@ -32528,42 +32708,45 @@ var CATEGORY_LABELS = {
32528
32708
  recent_receipts: "Recent receipts",
32529
32709
  verascore_deltas: "Verascore deltas"
32530
32710
  };
32531
- async function runFetcher(match, fetchers) {
32711
+ async function runFetcher(match, fetchers, hints) {
32532
32712
  switch (match.category) {
32533
32713
  case "templates":
32534
- return fetchers.templates();
32714
+ return fetchers.templates(hints);
32535
32715
  case "agent_state":
32536
- return fetchers.agent_state(match.agent_name_hint);
32716
+ return fetchers.agent_state(match.agent_name_hint, hints);
32537
32717
  case "agent_activity":
32538
- return fetchers.agent_activity(match.agent_name_hint);
32718
+ return fetchers.agent_activity(match.agent_name_hint, hints);
32539
32719
  case "audit_log":
32540
- return fetchers.audit_log();
32720
+ return fetchers.audit_log(hints);
32541
32721
  case "sentinel_findings":
32542
- return fetchers.sentinel_findings();
32722
+ return fetchers.sentinel_findings(hints);
32543
32723
  case "anomaly_alerts":
32544
- return fetchers.anomaly_alerts();
32724
+ return fetchers.anomaly_alerts(hints);
32545
32725
  case "recent_receipts":
32546
- return fetchers.recent_receipts();
32726
+ return fetchers.recent_receipts(hints);
32547
32727
  case "verascore_deltas":
32548
- return fetchers.verascore_deltas();
32728
+ return fetchers.verascore_deltas(hints);
32549
32729
  }
32550
32730
  }
32551
- function trivialMatch(category) {
32731
+ function trivialMatch(category, parsedGrammar) {
32552
32732
  return {
32553
32733
  category,
32554
32734
  confidence: 0.5,
32555
32735
  matched_keywords: ["llm-assist"],
32556
- agent_name_hint: null
32736
+ agent_name_hint: parsedGrammar && parsedGrammar.agent_names.length > 0 ? parsedGrammar.agent_names[0] ?? null : null,
32737
+ ...parsedGrammar !== void 0 ? { query_grammar: parsedGrammar } : {}
32557
32738
  };
32558
32739
  }
32559
32740
  async function foldContext(query, fetchers, opts) {
32560
32741
  const budget = opts?.maxTokens ?? DEFAULT_DYNAMIC_CONTEXT_TOKEN_BUDGET;
32561
- let matches = classifyQuery(query);
32742
+ const parsed = opts?.parsed ?? null;
32743
+ const hints = fetcherHintsFromGrammar(parsed);
32744
+ let matches = classifyQuery(query, parsed);
32562
32745
  if (matches.length === 0 && !isTrivialQuery(query) && opts?.llmAssistClassify) {
32563
32746
  try {
32564
32747
  const picked = await opts.llmAssistClassify(query, CONTEXT_CATEGORIES);
32565
32748
  if (picked !== "none" && CONTEXT_CATEGORIES.includes(picked)) {
32566
- matches = [trivialMatch(picked)];
32749
+ matches = [trivialMatch(picked, parsed)];
32567
32750
  }
32568
32751
  } catch {
32569
32752
  }
@@ -32574,7 +32757,7 @@ async function foldContext(query, fetchers, opts) {
32574
32757
  const attempts = [];
32575
32758
  for (const match of matches) {
32576
32759
  try {
32577
- const text = await runFetcher(match, fetchers);
32760
+ const text = await runFetcher(match, fetchers, hints);
32578
32761
  const trimmed = text.trim();
32579
32762
  if (trimmed.length > 0) {
32580
32763
  attempts.push({ category: match.category, text: trimmed });
@@ -32616,6 +32799,520 @@ ${blocks.join("\n\n")}`;
32616
32799
  };
32617
32800
  }
32618
32801
 
32802
+ // src/composition/constants.ts
32803
+ var COMPOSITION_EVENT_TYPES = [
32804
+ "composition_receipt_packed",
32805
+ "composition_receipt_verified",
32806
+ "composition_mandate_verified",
32807
+ "composition_verascore_published",
32808
+ "composition_sidecar_spawned",
32809
+ "composition_sidecar_crashed",
32810
+ "composition_sidecar_recovered",
32811
+ "composition_degraded",
32812
+ "composition_recovered"
32813
+ ];
32814
+
32815
+ // src/chat/concierge-query-grammar.ts
32816
+ var CANONICAL_AUDIT_EVENT_CLASSES = [
32817
+ // Lifecycle / policy
32818
+ "policy_change",
32819
+ "approval_request",
32820
+ "audit_truncate",
32821
+ "lockdown",
32822
+ "unwrap",
32823
+ // Exit bundle (Tier 1)
32824
+ "exit_bundle_export",
32825
+ "exit_bundle_import_activate",
32826
+ "exit_bundle_rekey",
32827
+ // Cross-harness approval aggregator
32828
+ "cross_harness_approval_aggregated",
32829
+ "cross_harness_approval_resolved",
32830
+ "cross_harness_approval_deduped",
32831
+ "cross_harness_approval_payload_decrypted",
32832
+ "cross_harness_approval_audit_trail_viewed",
32833
+ "cross_harness_approval_replayed",
32834
+ // Composition (full set from constants.ts)
32835
+ ...COMPOSITION_EVENT_TYPES,
32836
+ // Operator chat / concierge (full set from OPERATOR_CHAT_OPS)
32837
+ OPERATOR_CHAT_OPS.CONCIERGE_CHAT,
32838
+ OPERATOR_CHAT_OPS.AGENT_INSPECT_PANEL_OPENED,
32839
+ OPERATOR_CHAT_OPS.CONCIERGE_HISTORY_READ,
32840
+ OPERATOR_CHAT_OPS.CONCIERGE_THREAD_DELETED,
32841
+ OPERATOR_CHAT_OPS.CONCIERGE_MEMORY_READ_FAILED,
32842
+ OPERATOR_CHAT_OPS.CONCIERGE_CONTEXT_FETCHER_FAILED,
32843
+ // Bridge / commitment
32844
+ "bridge_commit",
32845
+ "bridge_verify",
32846
+ "bridge_attest",
32847
+ "proof_commitment",
32848
+ "proof_reveal",
32849
+ // Reputation
32850
+ "reputation_export",
32851
+ "reputation_import",
32852
+ "reputation_publish",
32853
+ "reputation_record",
32854
+ "reputation_query"
32855
+ ];
32856
+ var EVENT_SYNONYMS = [
32857
+ { phrase: "approvals", canonical: ["approval_request", "cross_harness_approval_aggregated", "cross_harness_approval_resolved"] },
32858
+ { phrase: "approval", canonical: ["approval_request"] },
32859
+ { phrase: "policy changes", canonical: ["policy_change"] },
32860
+ { phrase: "policy change", canonical: ["policy_change"] },
32861
+ { phrase: "policy edits", canonical: ["policy_change"] },
32862
+ { phrase: "lockdowns", canonical: ["lockdown"] },
32863
+ { phrase: "exit bundles", canonical: ["exit_bundle_export", "exit_bundle_import_activate"] },
32864
+ { phrase: "exit bundle", canonical: ["exit_bundle_export"] },
32865
+ { phrase: "audit truncations", canonical: ["audit_truncate"] },
32866
+ { phrase: "audit truncation", canonical: ["audit_truncate"] },
32867
+ { phrase: "compositions", canonical: ["composition_receipt_packed", "composition_receipt_verified"] },
32868
+ { phrase: "receipts", canonical: ["composition_receipt_packed", "composition_receipt_verified"] },
32869
+ { phrase: "receipt verifications", canonical: ["composition_receipt_verified"] },
32870
+ { phrase: "concierge chats", canonical: [OPERATOR_CHAT_OPS.CONCIERGE_CHAT] },
32871
+ { phrase: "cross harness approvals", canonical: ["cross_harness_approval_aggregated", "cross_harness_approval_resolved"] }
32872
+ ];
32873
+ var MS_PER_HOUR = 60 * 60 * 1e3;
32874
+ var MS_PER_DAY = 24 * MS_PER_HOUR;
32875
+ var NUMBER_WORDS = {
32876
+ a: 1,
32877
+ an: 1,
32878
+ one: 1,
32879
+ two: 2,
32880
+ three: 3,
32881
+ four: 4,
32882
+ five: 5,
32883
+ six: 6,
32884
+ seven: 7,
32885
+ eight: 8,
32886
+ nine: 9,
32887
+ ten: 10,
32888
+ twelve: 12,
32889
+ twentyfour: 24
32890
+ };
32891
+ function resolveTimeRange(query, now) {
32892
+ const normalized = query.trim();
32893
+ const lower = normalized.toLowerCase();
32894
+ const fromTo = lower.match(
32895
+ /\b(?:from|between)\s+(.+?)\s+(?:to|and|-|until)\s+([\w:.\-+t /]+)/i
32896
+ );
32897
+ if (fromTo) {
32898
+ const aSlice = fromTo[1];
32899
+ const bSlice = fromTo[2];
32900
+ if (aSlice !== void 0 && bSlice !== void 0) {
32901
+ const a = parseInstant(aSlice, now);
32902
+ const b = parseInstant(bSlice, now);
32903
+ if (a && b) {
32904
+ const start = a.getTime() <= b.getTime() ? a : b;
32905
+ const end = a.getTime() <= b.getTime() ? b : a;
32906
+ return {
32907
+ range: { start, end },
32908
+ matchedSubstring: fromTo[0]
32909
+ };
32910
+ }
32911
+ }
32912
+ }
32913
+ const sinceMatch = lower.match(/\bsince\s+([\w:.\-+t /]+)/i);
32914
+ if (sinceMatch) {
32915
+ const slice = sinceMatch[1];
32916
+ if (slice !== void 0) {
32917
+ const start = parseInstant(slice, now);
32918
+ if (start) {
32919
+ return {
32920
+ range: { start, end: now },
32921
+ matchedSubstring: sinceMatch[0]
32922
+ };
32923
+ }
32924
+ }
32925
+ }
32926
+ if (/\byesterday\b/.test(lower)) {
32927
+ const startOfToday = startOfDay(now);
32928
+ const start = new Date(startOfToday.getTime() - MS_PER_DAY);
32929
+ const end = new Date(startOfToday.getTime() - 1);
32930
+ return {
32931
+ range: { start, end, relative_label: "yesterday" },
32932
+ matchedSubstring: "yesterday"
32933
+ };
32934
+ }
32935
+ if (/\btoday\b/.test(lower)) {
32936
+ return {
32937
+ range: {
32938
+ start: startOfDay(now),
32939
+ end: now,
32940
+ relative_label: "today"
32941
+ },
32942
+ matchedSubstring: "today"
32943
+ };
32944
+ }
32945
+ const compactHours = lower.match(/\blast\s+(\d+)\s*h\b/i);
32946
+ if (compactHours) {
32947
+ const tok = compactHours[1];
32948
+ if (tok !== void 0) {
32949
+ const n = Number.parseInt(tok, 10);
32950
+ if (Number.isFinite(n) && n > 0) {
32951
+ const start = new Date(now.getTime() - n * MS_PER_HOUR);
32952
+ return {
32953
+ range: { start, end: now, relative_label: `last ${n}h` },
32954
+ matchedSubstring: compactHours[0]
32955
+ };
32956
+ }
32957
+ }
32958
+ }
32959
+ const hoursMatch = lower.match(
32960
+ /\b(?:past|last)\s+([\w]+|\d+)\s*(?:hr\b|hrs\b|hour|hours)/i
32961
+ );
32962
+ if (hoursMatch) {
32963
+ const tok = hoursMatch[1];
32964
+ if (tok !== void 0) {
32965
+ const n = parseCount(tok);
32966
+ if (n !== null && n > 0) {
32967
+ const start = new Date(now.getTime() - n * MS_PER_HOUR);
32968
+ return {
32969
+ range: { start, end: now, relative_label: `past ${n} hour${n === 1 ? "" : "s"}` },
32970
+ matchedSubstring: hoursMatch[0]
32971
+ };
32972
+ }
32973
+ }
32974
+ }
32975
+ if (/\b(?:past|last)\s+hour\b/.test(lower)) {
32976
+ const start = new Date(now.getTime() - MS_PER_HOUR);
32977
+ return {
32978
+ range: { start, end: now, relative_label: "past hour" },
32979
+ matchedSubstring: lower.match(/\b(?:past|last)\s+hour\b/i)[0]
32980
+ };
32981
+ }
32982
+ const daysMatch = lower.match(
32983
+ /\b(?:past|last)\s+([\w]+|\d+)\s*(?:d\b|day|days)/i
32984
+ );
32985
+ if (daysMatch) {
32986
+ const tok = daysMatch[1];
32987
+ if (tok !== void 0) {
32988
+ const n = parseCount(tok);
32989
+ if (n !== null && n > 0) {
32990
+ const start = new Date(now.getTime() - n * MS_PER_DAY);
32991
+ return {
32992
+ range: { start, end: now, relative_label: `past ${n} day${n === 1 ? "" : "s"}` },
32993
+ matchedSubstring: daysMatch[0]
32994
+ };
32995
+ }
32996
+ }
32997
+ }
32998
+ if (/\b(?:past|last)\s+day\b/.test(lower)) {
32999
+ const start = new Date(now.getTime() - MS_PER_DAY);
33000
+ return {
33001
+ range: { start, end: now, relative_label: "past day" },
33002
+ matchedSubstring: lower.match(/\b(?:past|last)\s+day\b/i)[0]
33003
+ };
33004
+ }
33005
+ if (/\bthis\s+week\b/.test(lower)) {
33006
+ const start = startOfWeek(now);
33007
+ return {
33008
+ range: { start, end: now, relative_label: "this week" },
33009
+ matchedSubstring: lower.match(/\bthis\s+week\b/i)[0]
33010
+ };
33011
+ }
33012
+ if (/\b(?:past|last)\s+week\b/.test(lower)) {
33013
+ const start = new Date(now.getTime() - 7 * MS_PER_DAY);
33014
+ return {
33015
+ range: { start, end: now, relative_label: "past week" },
33016
+ matchedSubstring: lower.match(/\b(?:past|last)\s+week\b/i)[0]
33017
+ };
33018
+ }
33019
+ const isoMatch = normalized.match(
33020
+ /\b(\d{4}-\d{2}-\d{2}(?:[T ]\d{2}:\d{2}(?::\d{2})?(?:Z|[+-]\d{2}:\d{2})?)?)\b/
33021
+ );
33022
+ if (isoMatch) {
33023
+ const tok = isoMatch[1];
33024
+ if (tok !== void 0) {
33025
+ const parsed = parseInstant(tok, now);
33026
+ if (parsed) {
33027
+ const isDateOnly = /^\d{4}-\d{2}-\d{2}$/.test(tok);
33028
+ if (isDateOnly) {
33029
+ return {
33030
+ range: {
33031
+ start: parsed,
33032
+ end: new Date(parsed.getTime() + MS_PER_DAY - 1)
33033
+ },
33034
+ matchedSubstring: tok
33035
+ };
33036
+ }
33037
+ return {
33038
+ range: {
33039
+ start: new Date(parsed.getTime() - 30 * 60 * 1e3),
33040
+ end: new Date(parsed.getTime() + 30 * 60 * 1e3)
33041
+ },
33042
+ matchedSubstring: tok
33043
+ };
33044
+ }
33045
+ }
33046
+ }
33047
+ return null;
33048
+ }
33049
+ function parseInstant(token, now) {
33050
+ const trimmed = token.trim().replace(/[,.!?;]+$/g, "");
33051
+ if (!trimmed) return null;
33052
+ const lower = trimmed.toLowerCase();
33053
+ if (lower === "now") return now;
33054
+ if (lower === "today") return startOfDay(now);
33055
+ if (lower === "yesterday") {
33056
+ return new Date(startOfDay(now).getTime() - MS_PER_DAY);
33057
+ }
33058
+ const isoLike = trimmed.replace(" ", "T");
33059
+ const parsed = new Date(isoLike);
33060
+ if (!Number.isNaN(parsed.getTime())) return parsed;
33061
+ return null;
33062
+ }
33063
+ function parseCount(token) {
33064
+ const lower = token.toLowerCase();
33065
+ if (/^\d+$/.test(lower)) {
33066
+ const n = Number.parseInt(lower, 10);
33067
+ return Number.isFinite(n) ? n : null;
33068
+ }
33069
+ return NUMBER_WORDS[lower] ?? null;
33070
+ }
33071
+ function startOfDay(d) {
33072
+ const out = new Date(d);
33073
+ out.setHours(0, 0, 0, 0);
33074
+ return out;
33075
+ }
33076
+ function startOfWeek(d) {
33077
+ const out = startOfDay(d);
33078
+ const dayOfWeek = out.getDay();
33079
+ const offsetToMonday = (dayOfWeek + 6) % 7;
33080
+ out.setDate(out.getDate() - offsetToMonday);
33081
+ return out;
33082
+ }
33083
+ function listFromRegistry(registry) {
33084
+ if (!registry) return [];
33085
+ if (Array.isArray(registry)) return registry;
33086
+ if (typeof registry.list === "function") {
33087
+ return registry.list();
33088
+ }
33089
+ return [];
33090
+ }
33091
+ function extractAgentNames(query, registry) {
33092
+ const records = listFromRegistry(registry);
33093
+ if (records.length === 0) return { matched: [], flagged: false };
33094
+ const lowerQuery = query.toLowerCase();
33095
+ const compactQuery = lowerQuery.replace(/[\s_-]+/g, "");
33096
+ const matched = [];
33097
+ const seen = /* @__PURE__ */ new Set();
33098
+ for (const rec of records) {
33099
+ const id = rec.agent_id;
33100
+ if (!id || seen.has(id)) continue;
33101
+ const idLower = id.toLowerCase();
33102
+ if (idLower.length < 3) continue;
33103
+ const idCompact = idLower.replace(/[\s_-]+/g, "");
33104
+ const wordRe = new RegExp(
33105
+ `\\b${idLower.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`,
33106
+ "i"
33107
+ );
33108
+ if (wordRe.test(query)) {
33109
+ matched.push(id);
33110
+ seen.add(id);
33111
+ continue;
33112
+ }
33113
+ if (idCompact.length >= 4 && compactQuery.includes(idCompact)) {
33114
+ matched.push(id);
33115
+ seen.add(id);
33116
+ }
33117
+ }
33118
+ const agentMention = lowerQuery.match(/\bagent\s+([a-z0-9_-]{3,40})/i);
33119
+ const flagged = matched.length === 0 && agentMention !== null && agentMention[1] !== void 0 && !records.some((r) => r.agent_id.toLowerCase() === agentMention[1]?.toLowerCase());
33120
+ return { matched, flagged };
33121
+ }
33122
+ function escapeRegex(s) {
33123
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
33124
+ }
33125
+ function extractEventTypes(query, enumValues) {
33126
+ const lower = query.toLowerCase();
33127
+ const matched = [];
33128
+ const seen = /* @__PURE__ */ new Set();
33129
+ for (const ev of enumValues) {
33130
+ if (seen.has(ev)) continue;
33131
+ const re = new RegExp(`\\b${escapeRegex(ev)}\\b`, "i");
33132
+ if (re.test(query)) {
33133
+ matched.push(ev);
33134
+ seen.add(ev);
33135
+ }
33136
+ }
33137
+ for (const syn of EVENT_SYNONYMS) {
33138
+ const re = new RegExp(
33139
+ `\\b${syn.phrase.split(/\s+/).map(escapeRegex).join("\\s+")}\\b`,
33140
+ "i"
33141
+ );
33142
+ if (re.test(query)) {
33143
+ for (const c of syn.canonical) {
33144
+ if (seen.has(c)) continue;
33145
+ if (!enumValues.includes(c)) continue;
33146
+ matched.push(c);
33147
+ seen.add(c);
33148
+ }
33149
+ }
33150
+ }
33151
+ const globMatches = lower.match(/\b([a-z_]+)_\*/g) ?? [];
33152
+ for (const glob of globMatches) {
33153
+ const prefix = glob.slice(0, -2);
33154
+ for (const ev of enumValues) {
33155
+ if (seen.has(ev)) continue;
33156
+ if (ev.startsWith(prefix)) {
33157
+ matched.push(ev);
33158
+ seen.add(ev);
33159
+ }
33160
+ }
33161
+ }
33162
+ const eventNounMention = /\b(?:event|events|class|classes)\b/i.test(query) && matched.length === 0;
33163
+ return { matched, flagged: eventNounMention };
33164
+ }
33165
+ function deriveIntentPhrase(query, stripTokens) {
33166
+ let out = query;
33167
+ for (const tok of stripTokens) {
33168
+ if (!tok) continue;
33169
+ const re = new RegExp(escapeRegex(tok), "gi");
33170
+ out = out.replace(re, " ");
33171
+ }
33172
+ return out.replace(/\s+/g, " ").trim();
33173
+ }
33174
+ function computeConfidence(parsed) {
33175
+ const dims = [
33176
+ { present: parsed.hasTimeMention, resolved: parsed.timeResolved },
33177
+ { present: parsed.hasAgentMention, resolved: parsed.agentResolved },
33178
+ { present: parsed.hasEventMention, resolved: parsed.eventResolved }
33179
+ ];
33180
+ const present = dims.filter((d) => d.present);
33181
+ let base;
33182
+ if (present.length === 0) {
33183
+ base = parsed.intentEmpty ? 0 : 0.3;
33184
+ } else {
33185
+ const resolved = present.filter((d) => d.resolved).length;
33186
+ base = resolved / present.length;
33187
+ }
33188
+ const adjusted = base - 0.15 * parsed.ambiguityCount;
33189
+ if (adjusted < 0) return 0;
33190
+ if (adjusted > 1) return 1;
33191
+ return adjusted;
33192
+ }
33193
+ 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;
33194
+ var AGENT_MENTION_PROBE = /\bagent[s]?\b/i;
33195
+ var EVENT_MENTION_PROBE = /\b(event|events|class|classes|approvals?|policy)\b/i;
33196
+ function parseQuery(query, opts) {
33197
+ const now = opts?.now ?? /* @__PURE__ */ new Date();
33198
+ const enumValues = opts?.eventClassEnum ?? CANONICAL_AUDIT_EVENT_CLASSES;
33199
+ const original = query ?? "";
33200
+ const trimmed = original.trim();
33201
+ if (trimmed.length === 0) {
33202
+ return {
33203
+ time_range: null,
33204
+ agent_names: [],
33205
+ event_types: [],
33206
+ intent_phrase: "",
33207
+ ambiguity_flags: ["no_signal_extracted"],
33208
+ parse_confidence: 0
33209
+ };
33210
+ }
33211
+ const ambiguity_flags = /* @__PURE__ */ new Set();
33212
+ const timeMatch = resolveTimeRange(trimmed, now);
33213
+ const hasTimeMention = TIME_MENTION_PROBE.test(trimmed);
33214
+ if (hasTimeMention && !timeMatch) {
33215
+ ambiguity_flags.add("unknown_time_token");
33216
+ }
33217
+ const agentResult = extractAgentNames(trimmed, opts?.registry);
33218
+ if (agentResult.flagged) {
33219
+ ambiguity_flags.add("unknown_agent_token");
33220
+ }
33221
+ const hasAgentMention = AGENT_MENTION_PROBE.test(trimmed);
33222
+ const eventResult = extractEventTypes(trimmed, enumValues);
33223
+ const hasEventMention = EVENT_MENTION_PROBE.test(trimmed);
33224
+ if (eventResult.flagged) {
33225
+ ambiguity_flags.add("unknown_event_token");
33226
+ }
33227
+ const stripTokens = [];
33228
+ if (timeMatch) stripTokens.push(timeMatch.matchedSubstring);
33229
+ for (const name of agentResult.matched) stripTokens.push(name);
33230
+ for (const ev of eventResult.matched) {
33231
+ if (trimmed.toLowerCase().includes(ev.toLowerCase())) {
33232
+ stripTokens.push(ev);
33233
+ }
33234
+ }
33235
+ const intent_phrase = deriveIntentPhrase(trimmed, stripTokens);
33236
+ const parse_confidence = computeConfidence({
33237
+ hasTimeMention,
33238
+ timeResolved: timeMatch !== null,
33239
+ hasAgentMention,
33240
+ agentResolved: agentResult.matched.length > 0,
33241
+ hasEventMention,
33242
+ eventResolved: eventResult.matched.length > 0,
33243
+ intentEmpty: intent_phrase.length === 0,
33244
+ ambiguityCount: ambiguity_flags.size
33245
+ });
33246
+ if (timeMatch === null && agentResult.matched.length === 0 && eventResult.matched.length === 0 && intent_phrase.length === 0) {
33247
+ ambiguity_flags.add("no_signal_extracted");
33248
+ }
33249
+ return {
33250
+ time_range: timeMatch ? timeMatch.range : null,
33251
+ agent_names: agentResult.matched,
33252
+ event_types: eventResult.matched,
33253
+ intent_phrase,
33254
+ ambiguity_flags: Array.from(ambiguity_flags),
33255
+ parse_confidence
33256
+ };
33257
+ }
33258
+ var LLM_ASSIST_THRESHOLD = 0.5;
33259
+ function isLowConfidence(parsed) {
33260
+ return parsed.parse_confidence < LLM_ASSIST_THRESHOLD;
33261
+ }
33262
+ async function parseQueryWithLlmAssist(query, llmAssist, opts) {
33263
+ const parsed = parseQuery(query, opts);
33264
+ if (!llmAssist || !isLowConfidence(parsed)) return parsed;
33265
+ let completion;
33266
+ try {
33267
+ completion = await llmAssist(query, parsed);
33268
+ } catch {
33269
+ return parsed;
33270
+ }
33271
+ if (!completion || typeof completion !== "object") return parsed;
33272
+ const merged = { ...parsed };
33273
+ if (parsed.time_range === null && completion.time_range) {
33274
+ merged.time_range = completion.time_range;
33275
+ }
33276
+ if (parsed.agent_names.length === 0 && Array.isArray(completion.agent_names)) {
33277
+ merged.agent_names = completion.agent_names.filter(
33278
+ (s) => typeof s === "string" && s.length > 0
33279
+ );
33280
+ }
33281
+ if (parsed.event_types.length === 0 && Array.isArray(completion.event_types)) {
33282
+ const allowed = new Set(opts?.eventClassEnum ?? CANONICAL_AUDIT_EVENT_CLASSES);
33283
+ merged.event_types = completion.event_types.filter(
33284
+ (s) => typeof s === "string" && allowed.has(s)
33285
+ );
33286
+ }
33287
+ merged.parse_confidence = Math.max(
33288
+ parsed.parse_confidence,
33289
+ computeConfidence({
33290
+ hasTimeMention: TIME_MENTION_PROBE.test(query),
33291
+ timeResolved: merged.time_range !== null,
33292
+ hasAgentMention: AGENT_MENTION_PROBE.test(query),
33293
+ agentResolved: merged.agent_names.length > 0,
33294
+ hasEventMention: EVENT_MENTION_PROBE.test(query),
33295
+ eventResolved: merged.event_types.length > 0,
33296
+ intentEmpty: merged.intent_phrase.length === 0,
33297
+ ambiguityCount: merged.ambiguity_flags.length
33298
+ })
33299
+ );
33300
+ return merged;
33301
+ }
33302
+ function auditSafeSummary(parsed) {
33303
+ return {
33304
+ time_range: parsed.time_range ? {
33305
+ start_iso: parsed.time_range.start.toISOString(),
33306
+ end_iso: parsed.time_range.end.toISOString(),
33307
+ ...parsed.time_range.relative_label !== void 0 ? { relative_label: parsed.time_range.relative_label } : {}
33308
+ } : null,
33309
+ agent_names: [...parsed.agent_names],
33310
+ event_types: [...parsed.event_types],
33311
+ ambiguity_flags: [...parsed.ambiguity_flags],
33312
+ parse_confidence: parsed.parse_confidence
33313
+ };
33314
+ }
33315
+
32619
33316
  // src/chat/operator-chat-service.ts
32620
33317
  var DEFAULT_CONCIERGE_MAX_TOKENS = 512;
32621
33318
  var DEFAULT_CONCIERGE_HISTORY_WINDOW_TURNS = 10;
@@ -32670,6 +33367,8 @@ var OperatorChatService = class {
32670
33367
  contextFetchers;
32671
33368
  contextLlmAssist;
32672
33369
  dynamicContextBudget;
33370
+ agentRegistry;
33371
+ grammarLlmAssist;
32673
33372
  /**
32674
33373
  * In-memory thread_id assigned to the active concierge session.
32675
33374
  * The first sendConcierge call after construction allocates a fresh
@@ -32708,6 +33407,12 @@ var OperatorChatService = class {
32708
33407
  this.contextLlmAssist = deps.conciergeContextLlmAssist;
32709
33408
  }
32710
33409
  this.dynamicContextBudget = deps.conciergeDynamicContextBudget !== void 0 && deps.conciergeDynamicContextBudget > 0 ? deps.conciergeDynamicContextBudget : DEFAULT_CONCIERGE_DYNAMIC_CONTEXT_BUDGET;
33410
+ if (deps.conciergeAgentRegistry) {
33411
+ this.agentRegistry = deps.conciergeAgentRegistry;
33412
+ }
33413
+ if (deps.conciergeGrammarLlmAssist) {
33414
+ this.grammarLlmAssist = deps.conciergeGrammarLlmAssist;
33415
+ }
32711
33416
  }
32712
33417
  // ── Concierge ─────────────────────────────────────────────────────────
32713
33418
  /**
@@ -32766,6 +33471,7 @@ var OperatorChatService = class {
32766
33471
  await this.memory.appendTurn(threadId, "user", trimmed).catch(() => {
32767
33472
  });
32768
33473
  }
33474
+ const parsedGrammar = await this.runGrammarParse(filterResult.filtered);
32769
33475
  const start = Date.now();
32770
33476
  let conciergeBody;
32771
33477
  let servedBy = "disabled";
@@ -32784,7 +33490,8 @@ var OperatorChatService = class {
32784
33490
  outcome = "substrate_disabled";
32785
33491
  } else {
32786
33492
  const dynamicResult = await this.runDynamicContextFold(
32787
- filterResult.filtered
33493
+ filterResult.filtered,
33494
+ parsedGrammar
32788
33495
  );
32789
33496
  dynamicCategoriesIncluded = dynamicResult.categoriesIncluded;
32790
33497
  const context = await this.assembleConciergeContext(
@@ -32855,7 +33562,8 @@ var OperatorChatService = class {
32855
33562
  ...this.memory ? {
32856
33563
  prior_turns_folded: memoryReadFailureReason === null ? priorTurns.length : 0
32857
33564
  } : {},
32858
- ...this.contextFetchers ? { dynamic_context_categories: [...dynamicCategoriesIncluded] } : {}
33565
+ ...this.contextFetchers ? { dynamic_context_categories: [...dynamicCategoriesIncluded] } : {},
33566
+ parsed_grammar: auditSafeSummary(parsedGrammar)
32859
33567
  };
32860
33568
  this.emit(OPERATOR_CHAT_OPS.CONCIERGE_CHAT, payload, outcome === "ok" ? "success" : "failure");
32861
33569
  return {
@@ -33070,8 +33778,12 @@ ${inbox}`
33070
33778
  * proceeds with no fold. Returns the rendered section + the list of
33071
33779
  * categories whose data made it into the section (used for the
33072
33780
  * round-trip audit emission).
33781
+ *
33782
+ * Tau-4: receives the pre-parsed `ParsedQuery` and forwards it as the
33783
+ * `parsed` opt to `foldContext`, so fetchers see the structured
33784
+ * `FetcherHints` derived from it.
33073
33785
  */
33074
- async runDynamicContextFold(query) {
33786
+ async runDynamicContextFold(query, parsedGrammar) {
33075
33787
  if (!this.contextFetchers) {
33076
33788
  return { section: "", categoriesIncluded: [] };
33077
33789
  }
@@ -33080,10 +33792,24 @@ ${inbox}`
33080
33792
  ...this.contextLlmAssist ? { llmAssistClassify: this.contextLlmAssist } : {},
33081
33793
  onFetcherFailure: (category, error) => {
33082
33794
  this.emitContextFetcherFailed(category, classifyFetcherError(error));
33083
- }
33795
+ },
33796
+ parsed: parsedGrammar
33084
33797
  });
33085
33798
  return result;
33086
33799
  }
33800
+ /**
33801
+ * WP-V1.3-9 Tau-4: parse the (PII-filtered) operator query into a
33802
+ * `ParsedQuery`. Routes through the LLM-assist completion hook when
33803
+ * configured and the rule-based parse is below
33804
+ * `LLM_ASSIST_THRESHOLD`. Always returns a parse object (never
33805
+ * throws) so the audit emission can carry the result unconditionally.
33806
+ */
33807
+ async runGrammarParse(query) {
33808
+ return parseQueryWithLlmAssist(query, this.grammarLlmAssist, {
33809
+ ...this.agentRegistry !== void 0 ? { registry: this.agentRegistry } : {},
33810
+ eventClassEnum: CANONICAL_AUDIT_EVENT_CLASSES
33811
+ });
33812
+ }
33087
33813
  /**
33088
33814
  * Emit the WP-V1.3-9 Tau-3 fetcher-failure audit event. Pulled out
33089
33815
  * of the fold path so the dynamic-context handler stays readable.