@resolveio/server-lib 22.3.55 → 22.3.57

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@resolveio/server-lib",
3
- "version": "22.3.55",
3
+ "version": "22.3.57",
4
4
  "description": "",
5
5
  "scripts": {
6
6
  "package": "./build_package.sh",
@@ -2024,8 +2024,10 @@ async function executeAiAssistantCodexRun(payload: AiCodexRunInput, context: any
2024
2024
  details
2025
2025
  });
2026
2026
  };
2027
+ let requestClassification = classifyAssistantRequestType(message);
2028
+ let dataQuestion = requestClassification.dataQuestion;
2027
2029
  const progressTracker = createAssistantProgressTracker(assistantMessageId, initialProgress);
2028
- const streamProgress = createAssistantStreamProgressHandler(progressTracker);
2030
+ const streamProgress = createAssistantStreamProgressHandler(progressTracker, () => dataQuestion);
2029
2031
  const remainingRunBudgetMs = () => {
2030
2032
  if (!runDeadlineMs) {
2031
2033
  return null;
@@ -2063,8 +2065,6 @@ async function executeAiAssistantCodexRun(payload: AiCodexRunInput, context: any
2063
2065
  let toolResult: AiAssistantToolResult | null = null;
2064
2066
  let assistantDebug: Record<string, any> | null = null;
2065
2067
  let directiveSource: 'model' | 'forced' | 'none' = 'none';
2066
- let requestClassification = classifyAssistantRequestType(message);
2067
- let dataQuestion = requestClassification.dataQuestion;
2068
2068
  let lastDirective: AssistantMongoDirective | null = null;
2069
2069
  let heuristicDirectivePrecomputed: AssistantMongoDirective | null = null;
2070
2070
  let usedDeterministicHeuristicFastPath = false;
@@ -2793,6 +2793,22 @@ async function executeAiAssistantCodexRun(payload: AiCodexRunInput, context: any
2793
2793
  }
2794
2794
  }
2795
2795
  : directive;
2796
+ const effectiveDirectiveMetadata = effectiveDirective.metadata && typeof effectiveDirective.metadata === 'object'
2797
+ ? effectiveDirective.metadata
2798
+ : {};
2799
+ const directiveAcknowledgementText = normalizeOptionalString(effectiveDirectiveMetadata.acknowledgementText);
2800
+ const directiveProgressMessages = Array.isArray(effectiveDirectiveMetadata.progress)
2801
+ ? effectiveDirectiveMetadata.progress.map((entry: any) => normalizeOptionalString(entry)).filter(Boolean)
2802
+ : [];
2803
+ const deterministicDataIntentDirective = isAssistantAppDataIntentHeuristicDirective(effectiveDirective);
2804
+ if (deterministicDataIntentDirective) {
2805
+ toolTelemetry.intentId = normalizeOptionalString(effectiveDirectiveMetadata.intentId) || undefined;
2806
+ toolTelemetry.recipeUsed = normalizeOptionalString(effectiveDirectiveMetadata.recipeUsed) || undefined;
2807
+ toolTelemetry.acknowledgementText = directiveAcknowledgementText || undefined;
2808
+ if (!collectionOverride) {
2809
+ toolTelemetry.overrideBlockedReason = 'deterministic_data_intent_preserved_collection';
2810
+ }
2811
+ }
2796
2812
  const rankedCollections = Array.isArray(collectionRanking?.ranked)
2797
2813
  ? collectionRanking.ranked
2798
2814
  .map((entry: any) => normalizeOptionalString(entry?.name))
@@ -2820,9 +2836,22 @@ async function executeAiAssistantCodexRun(payload: AiCodexRunInput, context: any
2820
2836
  deadlineMs: runDeadlineMs || undefined,
2821
2837
  maxTimeMS: assistantMongoMaxTimeMs
2822
2838
  };
2839
+ if (isAssistantAppDataIntentHeuristicDirective(value)) {
2840
+ request.__assistantRetryState = {
2841
+ ...(request.__assistantRetryState || {}),
2842
+ disableCrossCollectionRetry: true,
2843
+ rootCollection: normalizeOptionalString(request.collection) || undefined
2844
+ };
2845
+ }
2823
2846
  return request;
2824
2847
  };
2825
- progressTracker.push('Grabbing Data');
2848
+ if (directiveAcknowledgementText) {
2849
+ progressTracker.push(directiveAcknowledgementText);
2850
+ }
2851
+ else {
2852
+ progressTracker.push(deterministicDataIntentDirective ? 'Pulling the requested data' : 'Grabbing Data');
2853
+ }
2854
+ directiveProgressMessages.slice(0, 2).forEach(messageText => progressTracker.push(messageText));
2826
2855
  let executionDirective = effectiveDirective;
2827
2856
  let probeFieldHints: string[] = [];
2828
2857
  try {
@@ -2838,7 +2867,9 @@ async function executeAiAssistantCodexRun(payload: AiCodexRunInput, context: any
2838
2867
  };
2839
2868
  }
2840
2869
  else {
2841
- const aggregateProbeDisabled = executionDirective.type === 'aggregate' && !alwaysAllowProbeEnabled;
2870
+ const aggregateProbeDisabled = executionDirective.type === 'aggregate'
2871
+ && !alwaysAllowProbeEnabled
2872
+ && !isAssistantAppDataIntentHeuristicDirective(executionDirective);
2842
2873
  const probeEnabled = AI_ASSISTANT_TOOL_MAX_STEPS > 1 && !aggregateProbeDisabled;
2843
2874
  const probeBudgetAllowed = alwaysAllowProbeEnabled
2844
2875
  || hasRunBudget('Grabbing Data: probe', AI_ASSISTANT_MONGO_MIN_STAGE_BUDGET_MS);
@@ -6759,8 +6790,253 @@ function isAssistantSchemaHoursHeuristicDirective(directive: AssistantMongoDirec
6759
6790
  return rawLine.includes('heuristic_agg(schema-hours-user-time)');
6760
6791
  }
6761
6792
 
6793
+ function isAssistantAppDataIntentHeuristicDirective(directive: AssistantMongoDirective | null): boolean {
6794
+ const rawLine = normalizeOptionalString(directive?.rawLine).toLowerCase();
6795
+ return rawLine.includes('heuristic_agg(app-data-intent:') || rawLine.includes('heuristic_read(app-data-intent:');
6796
+ }
6797
+
6798
+ function scoreAssistantAppDataIntent(messageText: string, intent: AiAssistantAppDataIntent): number {
6799
+ if ((intent.excludeTerms || []).some(term => containsAssistantHeuristicTerm(messageText, term))) {
6800
+ return 0;
6801
+ }
6802
+ return (intent.terms || []).reduce((score, term) => {
6803
+ if (!containsAssistantHeuristicTerm(messageText, term)) {
6804
+ return score;
6805
+ }
6806
+ return score + (term.includes(' ') ? 4 : 1);
6807
+ }, 0);
6808
+ }
6809
+
6810
+ function selectAssistantAppDataIntent(message: string, collectionNames: string[], appId?: string): AiAssistantAppDataIntent | null {
6811
+ const text = normalizeOptionalString(message).toLowerCase();
6812
+ if (!text) {
6813
+ return null;
6814
+ }
6815
+ const availableNames = new Set((Array.isArray(collectionNames) ? collectionNames : []).map(value => normalizeOptionalString(value)).filter(Boolean));
6816
+ const profile = resolveAssistantAppHeuristicProfile(appId);
6817
+ let selected: { intent: AiAssistantAppDataIntent; score: number } | null = null;
6818
+ profile.dataIntents.forEach((intent) => {
6819
+ if (!intent?.id || !intent.collection) {
6820
+ return;
6821
+ }
6822
+ if (availableNames.size && !availableNames.has(intent.collection)) {
6823
+ return;
6824
+ }
6825
+ const score = scoreAssistantAppDataIntent(text, intent);
6826
+ if (score <= 0) {
6827
+ return;
6828
+ }
6829
+ if (!selected || score > selected.score) {
6830
+ selected = { intent, score };
6831
+ }
6832
+ });
6833
+ return selected ? selected.intent : null;
6834
+ }
6835
+
6836
+ function extractAssistantDataIntentCustomerText(message: string): string {
6837
+ let value = normalizeOptionalString(message);
6838
+ if (!value) {
6839
+ return '';
6840
+ }
6841
+ value = value.replace(/\b(summarize|summary|show|list|give|pull|find|tell|me|about|of|the|a|an|and|amount|quantity|qty|total|totals|for|to|them|their|by)\b/gi, ' ');
6842
+ value = value.replace(/\b(work\s*orders?|wos?|jobs?|invoices?|invoice|billed|billing|revenue|charged|chemicals?|chemical|sent|shipped|shipment|shipments|delivered|planned|inventory|on\s+hand|stock|this\s+year|ytd|year\s+to\s+date|current\s+year)\b/gi, ' ');
6843
+ value = value.replace(/\b(last|past|current|completed|reviewed|closed)\b/gi, ' ');
6844
+ value = value.replace(/[^a-z0-9&.' -]+/gi, ' ');
6845
+ return normalizeOptionalString(value.replace(/\s+/g, ' '));
6846
+ }
6847
+
6848
+ function resolveAssistantDataIntentDateWindow(message: string): { start?: string; end?: string; year?: number } {
6849
+ const text = normalizeOptionalString(message).toLowerCase();
6850
+ if (!/\b(this\s+year|ytd|year\s+to\s+date|current\s+year)\b/.test(text)) {
6851
+ return {};
6852
+ }
6853
+ const now = new Date();
6854
+ const year = now.getUTCFullYear();
6855
+ return {
6856
+ start: new Date(Date.UTC(year, 0, 1, 0, 0, 0, 0)).toISOString(),
6857
+ end: new Date(Date.UTC(year, now.getUTCMonth(), now.getUTCDate(), 23, 59, 59, 999)).toISOString(),
6858
+ year
6859
+ };
6860
+ }
6861
+
6862
+ function buildAssistantDataIntentAcknowledgement(
6863
+ intent: AiAssistantAppDataIntent,
6864
+ customerText: string,
6865
+ dateWindow: { year?: number }
6866
+ ): string {
6867
+ const customer = customerText || 'the requested customer';
6868
+ const yearText = dateWindow.year ? ` for ${dateWindow.year}` : '';
6869
+ if (intent.id === 'snf_job_chemical_shipments') {
6870
+ return `I understand you want a summary of chemicals sent to ${customer} jobs${yearText}. Let me pull the work-order shipment data for you.`;
6871
+ }
6872
+ if (intent.id === 'snf_invoice_chemical_quantities') {
6873
+ return `I understand you want invoice and billed chemical details for ${customer}${yearText}. Let me pull the invoice data for you.`;
6874
+ }
6875
+ if (intent.id === 'snf_planned_job_chemicals') {
6876
+ return `I understand you want planned chemicals for ${customer} jobs${yearText}. Let me pull the job setup data for you.`;
6877
+ }
6878
+ return 'I understand the data you want. Let me pull the matching records for you.';
6879
+ }
6880
+
6881
+ function buildAssistantDataIntentMatchStage(
6882
+ customerText: string,
6883
+ dateField: string | undefined,
6884
+ dateWindow: { start?: string; end?: string },
6885
+ customerPaths: string[]
6886
+ ): Record<string, any> {
6887
+ const and: Record<string, any>[] = [];
6888
+ if (customerText) {
6889
+ and.push({
6890
+ $or: customerPaths.map(path => ({
6891
+ [path]: {
6892
+ $regex: customerText,
6893
+ $options: 'i'
6894
+ }
6895
+ }))
6896
+ });
6897
+ }
6898
+ if (dateField && dateWindow.start && dateWindow.end) {
6899
+ and.push({
6900
+ [dateField]: {
6901
+ $gte: dateWindow.start,
6902
+ $lte: dateWindow.end
6903
+ }
6904
+ });
6905
+ }
6906
+ return and.length ? { $match: { $and: and } } : { $match: {} };
6907
+ }
6908
+
6909
+ function buildAssistantAppDataIntentDirective(message: string, collectionNames: string[] = [], appId?: string): AssistantMongoDirective | null {
6910
+ const intent = selectAssistantAppDataIntent(message, collectionNames, appId);
6911
+ if (!intent) {
6912
+ return null;
6913
+ }
6914
+ const customerText = extractAssistantDataIntentCustomerText(message);
6915
+ const dateWindow = resolveAssistantDataIntentDateWindow(message);
6916
+ const acknowledgementText = buildAssistantDataIntentAcknowledgement(intent, customerText, dateWindow);
6917
+ const progress = intent.progress.map((entry) => {
6918
+ return entry
6919
+ .replace(/\{\{customer\}\}/g, customerText || 'the requested customer')
6920
+ .replace(/\{\{year\}\}/g, dateWindow.year ? String(dateWindow.year) : 'the requested date range');
6921
+ });
6922
+ const metadata = {
6923
+ intentId: intent.id,
6924
+ recipeUsed: intent.id,
6925
+ acknowledgementText,
6926
+ progress,
6927
+ assumptions: intent.assumptions,
6928
+ fallbackCollections: intent.fallbackCollections
6929
+ };
6930
+ const basePayload = (pipeline: Record<string, any>[]) => ({
6931
+ collection: intent.collection,
6932
+ permissionView: intent.permissionView || resolveDefaultAssistantPermissionView(intent.collection),
6933
+ pipeline,
6934
+ options: {
6935
+ allowDiskUse: true,
6936
+ limit: 20
6937
+ }
6938
+ });
6939
+ if (intent.id === 'snf_job_chemical_shipments') {
6940
+ const pipeline: Record<string, any>[] = [
6941
+ buildAssistantDataIntentMatchStage(customerText, intent.dateField || 'date_completed', dateWindow, ['customer', 'job.customer']),
6942
+ { $match: { status: { $in: ['Completed', 'Reviewed', 'Closed'] } } },
6943
+ { $unwind: '$chemicals' },
6944
+ { $match: { 'chemicals.shipped.quantity': { $gt: 0 } } },
6945
+ {
6946
+ $group: {
6947
+ _id: {
6948
+ id_job: '$job.id_job',
6949
+ job: '$job.job',
6950
+ chemical: '$chemicals.chemical',
6951
+ unit: '$chemicals.unit'
6952
+ },
6953
+ qty_sent: { $sum: '$chemicals.shipped.quantity' },
6954
+ work_order_ids: { $addToSet: '$_id' }
6955
+ }
6956
+ },
6957
+ {
6958
+ $project: {
6959
+ _id: 0,
6960
+ id_job: '$_id.id_job',
6961
+ job: '$_id.job',
6962
+ chemical: '$_id.chemical',
6963
+ unit: '$_id.unit',
6964
+ qty_sent: 1,
6965
+ work_order_count: { $size: '$work_order_ids' }
6966
+ }
6967
+ },
6968
+ { $sort: { qty_sent: -1, job: 1, chemical: 1 } }
6969
+ ];
6970
+ return { type: 'aggregate', payload: basePayload(pipeline), cleaned: '', rawLine: `HEURISTIC_AGG(app-data-intent:${intent.id})`, metadata };
6971
+ }
6972
+ if (intent.id === 'snf_invoice_chemical_quantities') {
6973
+ const pipeline: Record<string, any>[] = [
6974
+ buildAssistantDataIntentMatchStage(customerText, intent.dateField || 'date_created', dateWindow, ['customer', 'job.customer']),
6975
+ { $unwind: '$items_chemicals' },
6976
+ {
6977
+ $group: {
6978
+ _id: {
6979
+ id_job: '$id_job',
6980
+ job: '$job',
6981
+ chemical: '$items_chemicals.chemical',
6982
+ unit: '$items_chemicals.unit'
6983
+ },
6984
+ quantity: { $sum: '$items_chemicals.quantity' },
6985
+ price_total: { $sum: '$items_chemicals.price_total' },
6986
+ invoice_ids: { $addToSet: '$_id' }
6987
+ }
6988
+ },
6989
+ {
6990
+ $project: {
6991
+ _id: 0,
6992
+ id_job: '$_id.id_job',
6993
+ job: '$_id.job',
6994
+ chemical: '$_id.chemical',
6995
+ unit: '$_id.unit',
6996
+ quantity: 1,
6997
+ price_total: 1,
6998
+ invoice_count: { $size: '$invoice_ids' }
6999
+ }
7000
+ },
7001
+ { $sort: { price_total: -1, quantity: -1, job: 1 } }
7002
+ ];
7003
+ return { type: 'aggregate', payload: basePayload(pipeline), cleaned: '', rawLine: `HEURISTIC_AGG(app-data-intent:${intent.id})`, metadata };
7004
+ }
7005
+ if (intent.id === 'snf_planned_job_chemicals') {
7006
+ const pipeline: Record<string, any>[] = [
7007
+ buildAssistantDataIntentMatchStage(customerText, intent.dateField || 'date_job_created', dateWindow, ['customer', 'jca.customer']),
7008
+ { $unwind: '$planned_chemicals' },
7009
+ {
7010
+ $group: {
7011
+ _id: {
7012
+ id_job: '$_id',
7013
+ job: '$job',
7014
+ chemical: '$planned_chemicals.chemical',
7015
+ unit: '$planned_chemicals.unit'
7016
+ },
7017
+ planned_quantity: { $sum: '$planned_chemicals.quantity' }
7018
+ }
7019
+ },
7020
+ {
7021
+ $project: {
7022
+ _id: 0,
7023
+ id_job: '$_id.id_job',
7024
+ job: '$_id.job',
7025
+ chemical: '$_id.chemical',
7026
+ unit: '$_id.unit',
7027
+ planned_quantity: 1
7028
+ }
7029
+ },
7030
+ { $sort: { planned_quantity: -1, job: 1 } }
7031
+ ];
7032
+ return { type: 'aggregate', payload: basePayload(pipeline), cleaned: '', rawLine: `HEURISTIC_AGG(app-data-intent:${intent.id})`, metadata };
7033
+ }
7034
+ return null;
7035
+ }
7036
+
6762
7037
  function isAssistantDeterministicHeuristicDirective(directive: AssistantMongoDirective | null): boolean {
6763
- return isAssistantSchemaHoursHeuristicDirective(directive);
7038
+ return isAssistantSchemaHoursHeuristicDirective(directive)
7039
+ || isAssistantAppDataIntentHeuristicDirective(directive);
6764
7040
  }
6765
7041
 
6766
7042
  function buildAssistantHeuristicDirective(
@@ -6772,7 +7048,15 @@ function buildAssistantHeuristicDirective(
6772
7048
  if (!normalizeOptionalString(message)) {
6773
7049
  return null;
6774
7050
  }
6775
- void collectionNames;
7051
+ const appDataIntentDirective = resolveAssistantDeterministicHeuristicEnabled(
7052
+ AI_ASSISTANT_HEURISTIC_ID_APP_DATA_INTENT,
7053
+ appId
7054
+ )
7055
+ ? buildAssistantAppDataIntentDirective(message, collectionNames, appId)
7056
+ : null;
7057
+ if (appDataIntentDirective) {
7058
+ return appDataIntentDirective;
7059
+ }
6776
7060
  const schemaHoursDirective = resolveAssistantDeterministicHeuristicEnabled(
6777
7061
  AI_ASSISTANT_HEURISTIC_ID_SCHEMA_HOURS_USER_TIME,
6778
7062
  appId
@@ -7516,6 +7800,14 @@ function buildAssistantToolResultPayload(
7516
7800
  const verification = toolResponse?.verification && typeof toolResponse.verification === 'object'
7517
7801
  ? toolResponse.verification
7518
7802
  : undefined;
7803
+ const directiveMetadata = directive.metadata && typeof directive.metadata === 'object' ? directive.metadata : {};
7804
+ const mergedDebug = {
7805
+ ...(toolResponse?.debug && typeof toolResponse.debug === 'object' ? toolResponse.debug : {}),
7806
+ ...(normalizeOptionalString(directiveMetadata.intentId) ? { intentId: normalizeOptionalString(directiveMetadata.intentId) } : {}),
7807
+ ...(normalizeOptionalString(directiveMetadata.acknowledgementText) ? { acknowledgementText: normalizeOptionalString(directiveMetadata.acknowledgementText) } : {}),
7808
+ ...(normalizeOptionalString(directiveMetadata.recipeUsed) ? { recipeUsed: normalizeOptionalString(directiveMetadata.recipeUsed) } : {}),
7809
+ ...(Array.isArray(directiveMetadata.assumptions) && directiveMetadata.assumptions.length ? { assumptions: directiveMetadata.assumptions } : {})
7810
+ };
7519
7811
  const result: AiAssistantToolResult = {
7520
7812
  type: directive.type === 'aggregate' ? 'mongo_agg' : 'mongo_read',
7521
7813
  input: directivePayload,
@@ -7527,7 +7819,7 @@ function buildAssistantToolResultPayload(
7527
7819
  columns: responseDisplay.columns,
7528
7820
  truncated: responseDisplay.truncated,
7529
7821
  verification,
7530
- debug: toolResponse?.debug && typeof toolResponse.debug === 'object' ? toolResponse.debug : undefined
7822
+ debug: Object.keys(mergedDebug).length ? mergedDebug : undefined
7531
7823
  }
7532
7824
  };
7533
7825
  return {
@@ -7726,9 +8018,13 @@ function buildAssistantDisplayLeadSummary(display: AiAssistantDisplayTable | nul
7726
8018
  function buildAssistantToolFallbackResponse(result: AiAssistantToolResult): string {
7727
8019
  const rowCount = Math.max(0, Number(result.output.rowCount || 0));
7728
8020
  const source = normalizeOptionalString(result.output.collection);
8021
+ const debug = result.output.debug && typeof result.output.debug === 'object' ? result.output.debug : {};
8022
+ const intentId = normalizeOptionalString((debug as any).intentId);
7729
8023
  const lines: string[] = rowCount > 0
7730
8024
  ? [`I found ${rowCount === 1 ? '1 matching row' : `${rowCount} matching rows`}${source ? ` from ${source}` : ''}.`]
7731
- : [`The query ran successfully${source ? ` against ${source}` : ''}, but no matching rows were returned.`];
8025
+ : [intentId
8026
+ ? `I checked the ${source || 'selected'} data for that request, but I did not find matching rows for the filters I used.`
8027
+ : `I checked the data${source ? ` in ${source}` : ''}, but no matching rows were returned.`];
7732
8028
  if (source) {
7733
8029
  lines.push(`- Source: ${source}`);
7734
8030
  }
@@ -7736,6 +8032,22 @@ function buildAssistantToolFallbackResponse(result: AiAssistantToolResult): stri
7736
8032
  if (typeof result.output.total === 'number') {
7737
8033
  lines.push(`- Total: ${result.output.total}`);
7738
8034
  }
8035
+ if (rowCount <= 0) {
8036
+ const assumptions = Array.isArray((debug as any).assumptions)
8037
+ ? (debug as any).assumptions.map((entry: any) => normalizeOptionalString(entry)).filter(Boolean)
8038
+ : [];
8039
+ if (assumptions.length) {
8040
+ lines.push(`- Assumptions: ${assumptions.slice(0, 3).join('; ')}`);
8041
+ }
8042
+ if (intentId === 'snf_job_chemical_shipments') {
8043
+ lines.push('- I treated "sent" as shipped chemical quantity from completed/reviewed/closed work orders.');
8044
+ lines.push('- If you meant billed quantities or dollars, ask for invoiced/billed chemicals instead.');
8045
+ }
8046
+ else if (intentId === 'snf_invoice_chemical_quantities') {
8047
+ lines.push('- I treated the request as billed invoice chemical quantities and invoice totals.');
8048
+ lines.push('- If you meant field shipments, ask for chemicals shipped/sent on work orders.');
8049
+ }
8050
+ }
7739
8051
  const leadSummary = buildAssistantDisplayLeadSummary(result.output.display);
7740
8052
  if (leadSummary) {
7741
8053
  lines.push('');
@@ -8300,11 +8612,32 @@ function deriveAssistantStreamStatus(event: any): string | null {
8300
8612
  return null;
8301
8613
  }
8302
8614
 
8303
- function createAssistantStreamProgressHandler(progressTracker: CodexProgressTracker): CodexStreamStatusHandler {
8615
+ function normalizeAssistantStreamProgressStatus(status: string, dataMode = false): string {
8616
+ const trimmed = normalizeOptionalString(status);
8617
+ if (!trimmed || !dataMode) {
8618
+ return trimmed;
8619
+ }
8620
+ const normalized = trimmed.toLowerCase();
8621
+ if (normalized === 'planning') {
8622
+ return 'Understanding your request';
8623
+ }
8624
+ if (normalized === 'drafting response') {
8625
+ return 'Preparing the summary';
8626
+ }
8627
+ if (normalized === 'searching files' || normalized === 'finding files' || normalized === 'opening files' || normalized === 'scanning files') {
8628
+ return 'Checking available data';
8629
+ }
8630
+ return trimmed;
8631
+ }
8632
+
8633
+ function createAssistantStreamProgressHandler(
8634
+ progressTracker: CodexProgressTracker,
8635
+ isDataMode?: () => boolean
8636
+ ): CodexStreamStatusHandler {
8304
8637
  let lastStatus = '';
8305
8638
  let lastSentAt = 0;
8306
8639
  return (status: string) => {
8307
- const trimmed = normalizeOptionalString(status);
8640
+ const trimmed = normalizeAssistantStreamProgressStatus(status, isDataMode?.() === true);
8308
8641
  if (!trimmed) {
8309
8642
  return;
8310
8643
  }
@@ -16358,6 +16691,9 @@ function resolveCollectionOverrideWithContext(params: {
16358
16691
  ) {
16359
16692
  const routePreferredName = normalizeAssistantCollectionOverrideName(routePreferred);
16360
16693
  const requestedIsWorkOrder = isAssistantWorkOrderCollectionOverrideCandidate(requested);
16694
+ if (shouldPreserveAssistantJobDomainCollectionForRoute(params.message, permissionView, routePreferredName, requested)) {
16695
+ return null;
16696
+ }
16361
16697
  if (!(routePreferredName === 'orders' && requestedIsWorkOrder && hasAssistantWorkOrderAliasTerms(params.message))) {
16362
16698
  return {
16363
16699
  from: requested,
@@ -16484,6 +16820,33 @@ export function resolveCollectionOverrideWithContextForTesting(params: {
16484
16820
  return resolveCollectionOverrideWithContext(params);
16485
16821
  }
16486
16822
 
16823
+ function shouldPreserveAssistantJobDomainCollectionForRoute(
16824
+ message: string,
16825
+ permissionView: string,
16826
+ routePreferredName: string,
16827
+ requestedCollection: string
16828
+ ): boolean {
16829
+ const route = normalizeOptionalString(permissionView).toLowerCase();
16830
+ const requested = normalizeAssistantCollectionOverrideName(requestedCollection);
16831
+ if (!route || routePreferredName !== 'jobs') {
16832
+ return false;
16833
+ }
16834
+ if (!/\/(?:customer-info\/jobs|job\/list|jobs?)(?:\/|$)/.test(route)) {
16835
+ return false;
16836
+ }
16837
+ const text = normalizeOptionalString(message).toLowerCase();
16838
+ if (!text || !/\b(job|jobs|work\s*orders?|wos?)\b/.test(text)) {
16839
+ return false;
16840
+ }
16841
+ if (requested === 'work-order-dynamics') {
16842
+ return /\b(chemical|chemicals|sent|shipped|shipment|delivered|work\s*orders?|wos?)\b/.test(text);
16843
+ }
16844
+ if (requested === 'invoices') {
16845
+ return /\b(invoice|invoices|billed|billing|revenue|charged|dollars?)\b/.test(text);
16846
+ }
16847
+ return requested === 'jobs';
16848
+ }
16849
+
16487
16850
  function isAssistantWorkOrderCollectionOverrideCandidate(value: string): boolean {
16488
16851
  const normalized = normalizeAssistantCollectionOverrideName(value);
16489
16852
  return normalized === 'work-order-dynamics'
@@ -258,6 +258,7 @@ export function buildResolveIORunnerLocalQaScript(): string {
258
258
  'cleanup() {',
259
259
  ' [ "${RUNNER_REUSED_READY:-0}" = "1" ] && return 0',
260
260
  ' janitor_stop_heartbeat',
261
+ ' rm -f "$ARTIFACT_DIR/heartbeat.pid"',
261
262
  ' janitor_update_manifest_status cleanup_started',
262
263
  ' kill_tree "$SERVER_PID"',
263
264
  ' kill_tree "$CLIENT_PID"',
@@ -267,6 +268,15 @@ export function buildResolveIORunnerLocalQaScript(): string {
267
268
  ' rmdir "$LOCK_DIR" >/dev/null 2>&1 || true',
268
269
  '}',
269
270
  'trap cleanup EXIT',
271
+ 'detach_keepalive() {',
272
+ ' janitor_update_manifest_status ready_keepalive',
273
+ ' janitor_write_cleanup_status ready_keepalive 0',
274
+ ' rmdir "$LOCK_DIR" >/dev/null 2>&1 || true',
275
+ ' trap - EXIT',
276
+ ' disown >/dev/null 2>&1 || true',
277
+ ' echo "ResolveIO AI runner QA keepalive detached at $CLIENT_URL; stop with $TOOLS_DIR/stop-local-qa.sh $PROJECT_ROOT."',
278
+ ' exit 0',
279
+ '}',
270
280
  'probe_url() { node - "$1" <<\'RESOLVEIO_PROBE\'',
271
281
  'const http = require("http");',
272
282
  'const https = require("https");',
@@ -374,6 +384,7 @@ export function buildResolveIORunnerLocalQaScript(): string {
374
384
  ': > "$ARTIFACT_DIR/runner.log"',
375
385
  'janitor_write_manifest',
376
386
  'janitor_start_heartbeat',
387
+ 'echo "$RUNNER_JANITOR_HEARTBEAT_PID" > "$ARTIFACT_DIR/heartbeat.pid"',
377
388
  'janitor_check_resources || exit $?',
378
389
  'if [ -d "$PROJECT_ROOT/server" ]; then',
379
390
  ' SERVER_REQUIRED=1',
@@ -410,7 +421,7 @@ export function buildResolveIORunnerLocalQaScript(): string {
410
421
  ' echo "ResolveIO AI runner QA local app ready at $CLIENT_URL";',
411
422
  ' echo "$SERVER_PID" > "$ARTIFACT_DIR/server.pid";',
412
423
  ' echo "$CLIENT_PID" > "$ARTIFACT_DIR/client.pid";',
413
- ' if truthy "$KEEPALIVE"; then echo "ResolveIO AI runner QA keepalive enabled; server/client remain running until this process is stopped."; wait; fi;',
424
+ ' if truthy "$KEEPALIVE"; then detach_keepalive; fi;',
414
425
  ' exit 0',
415
426
  ' ;;',
416
427
  ' 2) echo "ResolveIO AI runner QA client process exited before $CLIENT_URL became ready. See $ARTIFACT_DIR/client.log"; exit 2 ;;',
@@ -483,7 +494,7 @@ export function buildResolveIORunnerLocalQaStopperScript(): string {
483
494
  '}',
484
495
  'janitor_update_manifest_status cleanup_started',
485
496
  'killed_count=0',
486
- 'for pid_file in "$ARTIFACT_DIR/server.pid" "$ARTIFACT_DIR/client.pid"; do',
497
+ 'for pid_file in "$ARTIFACT_DIR/heartbeat.pid" "$ARTIFACT_DIR/server.pid" "$ARTIFACT_DIR/client.pid"; do',
487
498
  ' [ -f "$pid_file" ] || continue',
488
499
  ' pid="$(cat "$pid_file" 2>/dev/null || true)"',
489
500
  ' kill_tree "$pid"',
@@ -693,7 +704,7 @@ export function buildResolveIORunnerQaToolsReadme(options: ResolveIORunnerQaTool
693
704
  `This workspace reserves Angular QA client port ${port}; use \`$${clientUrlVar}\` instead of assuming 4200 is free.`,
694
705
  'The local QA runner starts server/client, polls the reserved client URL, writes `qa-artifacts/server.log` and `qa-artifacts/client.log`, and fails fast on fatal startup/runtime errors.',
695
706
  'The shared auth bootstrap first opens the exact localhost client origin, logs out any visible stale session, clears service workers/cache/IndexedDB/local/session storage, then calls `/login` and `/accessToken`, seeds `refreshToken`, `accessToken`, `user`, and `lastURL`, and writes `qa-artifacts/auth-bootstrap-result.json` plus a ready/failure screenshot.',
696
- `For browser clickthrough work, start the runner once with \`${keepaliveVar}=true ${toolsDir}/run-local-qa.sh <project-root> &\` and reuse \`$${clientUrlVar}\` for all login/upload/screenshot retries. Do not restart Angular for auth failures.`,
707
+ `For browser clickthrough work, start the runner once with \`${keepaliveVar}=true ${toolsDir}/run-local-qa.sh <project-root>\`; it detaches after the app is ready, and later calls should reuse \`$${clientUrlVar}\` for all login/upload/screenshot retries. Do not restart Angular for auth failures.`,
697
708
  'Do not run `npm run build-dev`, `ng build`, or another Angular compile while keepalive `ng serve` is running. If a full Angular build is required after browser QA, first run the staged `stop-local-qa.sh`, then build, then restart `run-local-qa.sh` for final browser proof.',
698
709
  'Use desktop screenshots at 1920x1080 by default unless the task is explicitly mobile/responsive. Every screenshot must have a customer-facing caption.',
699
710
  'For import/export/form-submit/data workflows, prove before/action/after with representative data and a concrete row/count/value assertion.',