@superblocksteam/vite-plugin-file-sync 2.0.139 → 2.0.140-next.1

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.
Files changed (136) hide show
  1. package/dist/ai-service/agent/tools/apis/api-validation-orchestrator.js +1 -1
  2. package/dist/ai-service/agent/tools/apis/api-validation-orchestrator.js.map +1 -1
  3. package/dist/ai-service/agent/tools/apis/test-api.d.ts.map +1 -1
  4. package/dist/ai-service/agent/tools/apis/test-api.js +3 -2
  5. package/dist/ai-service/agent/tools/apis/test-api.js.map +1 -1
  6. package/dist/ai-service/agent/tools2/tools/exit-plan-mode.d.ts.map +1 -1
  7. package/dist/ai-service/agent/tools2/tools/exit-plan-mode.js +1 -13
  8. package/dist/ai-service/agent/tools2/tools/exit-plan-mode.js.map +1 -1
  9. package/dist/ai-service/agent/tools2/tools/git.d.ts.map +1 -1
  10. package/dist/ai-service/agent/tools2/tools/git.js +19 -1
  11. package/dist/ai-service/agent/tools2/tools/git.js.map +1 -1
  12. package/dist/ai-service/agent/utils.d.ts.map +1 -1
  13. package/dist/ai-service/agent/utils.js +46 -3
  14. package/dist/ai-service/agent/utils.js.map +1 -1
  15. package/dist/ai-service/app-interface/npm-registry.d.ts +11 -7
  16. package/dist/ai-service/app-interface/npm-registry.d.ts.map +1 -1
  17. package/dist/ai-service/app-interface/npm-registry.js +11 -7
  18. package/dist/ai-service/app-interface/npm-registry.js.map +1 -1
  19. package/dist/ai-service/app-skills/helpers.d.ts.map +1 -1
  20. package/dist/ai-service/app-skills/helpers.js +2 -0
  21. package/dist/ai-service/app-skills/helpers.js.map +1 -1
  22. package/dist/ai-service/chat/chat-session-store-metrics.d.ts.map +1 -1
  23. package/dist/ai-service/chat/chat-session-store-metrics.js +26 -17
  24. package/dist/ai-service/chat/chat-session-store-metrics.js.map +1 -1
  25. package/dist/ai-service/chat/chat-session-store.d.ts +1 -4
  26. package/dist/ai-service/chat/chat-session-store.d.ts.map +1 -1
  27. package/dist/ai-service/chat/chat-session-store.js +32 -8
  28. package/dist/ai-service/chat/chat-session-store.js.map +1 -1
  29. package/dist/ai-service/chat/transcript-metrics.d.ts +5 -0
  30. package/dist/ai-service/chat/transcript-metrics.d.ts.map +1 -1
  31. package/dist/ai-service/chat/transcript-metrics.js +42 -15
  32. package/dist/ai-service/chat/transcript-metrics.js.map +1 -1
  33. package/dist/ai-service/index.d.ts +2 -0
  34. package/dist/ai-service/index.d.ts.map +1 -1
  35. package/dist/ai-service/index.js +206 -31
  36. package/dist/ai-service/index.js.map +1 -1
  37. package/dist/ai-service/llm/client.d.ts.map +1 -1
  38. package/dist/ai-service/llm/client.js +9 -9
  39. package/dist/ai-service/llm/client.js.map +1 -1
  40. package/dist/ai-service/llm/context-v2/context-metrics.d.ts +6 -0
  41. package/dist/ai-service/llm/context-v2/context-metrics.d.ts.map +1 -1
  42. package/dist/ai-service/llm/context-v2/context-metrics.js +18 -1
  43. package/dist/ai-service/llm/context-v2/context-metrics.js.map +1 -1
  44. package/dist/ai-service/llm/provider.d.ts +7 -1
  45. package/dist/ai-service/llm/provider.d.ts.map +1 -1
  46. package/dist/ai-service/llm/provider.js +292 -15
  47. package/dist/ai-service/llm/provider.js.map +1 -1
  48. package/dist/ai-service/llm/stream/empty-tool-call-tracker.d.ts +47 -0
  49. package/dist/ai-service/llm/stream/empty-tool-call-tracker.d.ts.map +1 -0
  50. package/dist/ai-service/llm/stream/empty-tool-call-tracker.js +122 -0
  51. package/dist/ai-service/llm/stream/empty-tool-call-tracker.js.map +1 -0
  52. package/dist/ai-service/llm/stream/errors.d.ts +21 -1
  53. package/dist/ai-service/llm/stream/errors.d.ts.map +1 -1
  54. package/dist/ai-service/llm/stream/errors.js +20 -0
  55. package/dist/ai-service/llm/stream/errors.js.map +1 -1
  56. package/dist/ai-service/llm/stream/managed-stream.d.ts +5 -1
  57. package/dist/ai-service/llm/stream/managed-stream.d.ts.map +1 -1
  58. package/dist/ai-service/llm/stream/managed-stream.js +51 -1
  59. package/dist/ai-service/llm/stream/managed-stream.js.map +1 -1
  60. package/dist/ai-service/llm/stream/orchestrator.d.ts +17 -2
  61. package/dist/ai-service/llm/stream/orchestrator.d.ts.map +1 -1
  62. package/dist/ai-service/llm/stream/orchestrator.js +185 -135
  63. package/dist/ai-service/llm/stream/orchestrator.js.map +1 -1
  64. package/dist/ai-service/llmobs/otel-exporter.d.ts.map +1 -1
  65. package/dist/ai-service/llmobs/otel-exporter.js +17 -6
  66. package/dist/ai-service/llmobs/otel-exporter.js.map +1 -1
  67. package/dist/ai-service/llmobs/tracer.d.ts +24 -2
  68. package/dist/ai-service/llmobs/tracer.d.ts.map +1 -1
  69. package/dist/ai-service/llmobs/tracer.js +90 -25
  70. package/dist/ai-service/llmobs/tracer.js.map +1 -1
  71. package/dist/ai-service/llmobs/types.d.ts +5 -0
  72. package/dist/ai-service/llmobs/types.d.ts.map +1 -1
  73. package/dist/ai-service/state-machine/clark-fsm.d.ts +24 -7
  74. package/dist/ai-service/state-machine/clark-fsm.d.ts.map +1 -1
  75. package/dist/ai-service/state-machine/clark-fsm.js +5 -0
  76. package/dist/ai-service/state-machine/clark-fsm.js.map +1 -1
  77. package/dist/ai-service/state-machine/handlers/agent-planning.d.ts.map +1 -1
  78. package/dist/ai-service/state-machine/handlers/agent-planning.js +30 -43
  79. package/dist/ai-service/state-machine/handlers/agent-planning.js.map +1 -1
  80. package/dist/ai-service/state-machine/handlers/awaiting-user.d.ts.map +1 -1
  81. package/dist/ai-service/state-machine/handlers/awaiting-user.js +21 -0
  82. package/dist/ai-service/state-machine/handlers/awaiting-user.js.map +1 -1
  83. package/dist/ai-service/state-machine/handlers/idle.d.ts.map +1 -1
  84. package/dist/ai-service/state-machine/handlers/idle.js +16 -1
  85. package/dist/ai-service/state-machine/handlers/idle.js.map +1 -1
  86. package/dist/ai-service/state-machine/handlers/llm-generating.d.ts.map +1 -1
  87. package/dist/ai-service/state-machine/handlers/llm-generating.js +10 -21
  88. package/dist/ai-service/state-machine/handlers/llm-generating.js.map +1 -1
  89. package/dist/ai-service/state-machine/handlers/persist-prompt-attachments.d.ts +25 -0
  90. package/dist/ai-service/state-machine/handlers/persist-prompt-attachments.d.ts.map +1 -0
  91. package/dist/ai-service/state-machine/handlers/persist-prompt-attachments.js +68 -0
  92. package/dist/ai-service/state-machine/handlers/persist-prompt-attachments.js.map +1 -0
  93. package/dist/ai-service/state-machine/helpers/prompt-failure.d.ts +51 -0
  94. package/dist/ai-service/state-machine/helpers/prompt-failure.d.ts.map +1 -0
  95. package/dist/ai-service/state-machine/helpers/prompt-failure.js +149 -0
  96. package/dist/ai-service/state-machine/helpers/prompt-failure.js.map +1 -0
  97. package/dist/ai-service/state-machine/helpers/prompt-request-metrics.d.ts +77 -0
  98. package/dist/ai-service/state-machine/helpers/prompt-request-metrics.d.ts.map +1 -0
  99. package/dist/ai-service/state-machine/helpers/prompt-request-metrics.js +229 -0
  100. package/dist/ai-service/state-machine/helpers/prompt-request-metrics.js.map +1 -0
  101. package/dist/ai-service/state-machine/helpers/transition.d.ts.map +1 -1
  102. package/dist/ai-service/state-machine/helpers/transition.js +5 -1
  103. package/dist/ai-service/state-machine/helpers/transition.js.map +1 -1
  104. package/dist/ai-service/state-machine/traced-fsm.d.ts +6 -1
  105. package/dist/ai-service/state-machine/traced-fsm.d.ts.map +1 -1
  106. package/dist/ai-service/state-machine/traced-fsm.js +13 -3
  107. package/dist/ai-service/state-machine/traced-fsm.js.map +1 -1
  108. package/dist/ai-service/util/llm-config-utils.d.ts +2 -2
  109. package/dist/ai-service/util/llm-config-utils.d.ts.map +1 -1
  110. package/dist/ai-service/util/llm-config-utils.js +2 -2
  111. package/dist/ai-service/util/llm-config-utils.js.map +1 -1
  112. package/dist/git-service/app-layout-guard.d.ts +36 -0
  113. package/dist/git-service/app-layout-guard.d.ts.map +1 -0
  114. package/dist/git-service/app-layout-guard.js +66 -0
  115. package/dist/git-service/app-layout-guard.js.map +1 -0
  116. package/dist/git-service/errors.d.ts +5 -0
  117. package/dist/git-service/errors.d.ts.map +1 -1
  118. package/dist/git-service/errors.js +10 -0
  119. package/dist/git-service/errors.js.map +1 -1
  120. package/dist/git-service/index.d.ts +2 -2
  121. package/dist/git-service/index.d.ts.map +1 -1
  122. package/dist/git-service/index.js +2 -2
  123. package/dist/git-service/index.js.map +1 -1
  124. package/dist/migration-templates/app-fullstack/package.json +1 -1
  125. package/dist/socket-manager.d.ts.map +1 -1
  126. package/dist/socket-manager.js +6 -0
  127. package/dist/socket-manager.js.map +1 -1
  128. package/dist/sync-service/index.d.ts +16 -9
  129. package/dist/sync-service/index.d.ts.map +1 -1
  130. package/dist/sync-service/index.js +154 -4
  131. package/dist/sync-service/index.js.map +1 -1
  132. package/dist/util/logger.d.ts +7 -0
  133. package/dist/util/logger.d.ts.map +1 -1
  134. package/dist/util/logger.js +29 -0
  135. package/dist/util/logger.js.map +1 -1
  136. package/package.json +14 -13
@@ -1,7 +1,8 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
- import { generateObject, stepCountIs, streamText as aiStreamText, } from "ai";
4
+ import { context as otelContext, propagation, SpanKind, SpanStatusCode, trace, } from "@opentelemetry/api";
5
+ import { stepCountIs, streamText as aiStreamText, } from "ai";
5
6
  import { z } from "zod";
6
7
  import { AiGenerationState, AiMode, resolveAttachmentFileName, } from "@superblocksteam/library-shared/types";
7
8
  import { addTracingToMethods, classifyAttachmentDelivery, FactAccessType, FactCreatedSource, FactEntityScope, TracedEventEmitter, getAiQuotaPaywallReasonFromMessage, } from "@superblocksteam/shared";
@@ -47,7 +48,7 @@ import { selectContextManagementStrategy, } from "./llm/context-v2/context-manag
47
48
  import { ContextManagerV2 } from "./llm/context-v2/manager.js";
48
49
  import { JsonlEventStore } from "./llm/context-v2/storage/jsonl-event-store.js";
49
50
  import { LocalContextStorageV2 } from "./llm/context-v2/storage/local.js";
50
- import { createLLMProvider } from "./llm/provider.js";
51
+ import { createLLMProvider, setSpanError } from "./llm/provider.js";
51
52
  import { traceLLM } from "./llmobs/helpers.js";
52
53
  import { getJwtTraceTags } from "./llmobs/helpers.js";
53
54
  import llmobs from "./llmobs/index.js";
@@ -72,6 +73,7 @@ import { classificationHelper } from "./state-machine/helpers/classification.js"
72
73
  import { getContextId } from "./state-machine/helpers/context-id.js";
73
74
  import { sendUserGenerationStateChannel } from "./state-machine/helpers/peer.js";
74
75
  import { sendUserMessageChannel, clearPendingToolPermissionRequest, } from "./state-machine/helpers/peer.js";
76
+ import { promptFailureFromStreamAbort, promptFailurePaywall, promptFailurePlatform, promptFailureUserCanceled, recordPromptAttempt, recordPromptEntryRejection, recordPromptOutcome, recordAwaitingUserHandoffDismissed, resolvePromptOutcomeDedupeKey, resolvePromptRequestDedupeKey, setPromptRequestMetricIdentity, } from "./state-machine/helpers/prompt-request-metrics.js";
75
77
  import { catchBackgroundPeerDisconnect, StablePeer, } from "./state-machine/helpers/stable-peer.js";
76
78
  import { transitionFrom } from "./state-machine/helpers/transition.js";
77
79
  import { isTaskAccessibleTo, registerBackgroundTaskTypes, resyncActiveTaskStatusesToPeer, TaskStore, wireTaskStatusPushToPeer, } from "./tasks/index.js";
@@ -326,6 +328,7 @@ export class AiService extends TracedEventEmitter {
326
328
  requestDeduplicator = new RequestDeduplicator();
327
329
  entityPermissionStore = new SessionEntityPermissionStore();
328
330
  _onGenerationCompleteCallback;
331
+ activeGenerationTrace;
329
332
  /** JWT-derived userId of the currently-authenticated peer, set by
330
333
  * `notifyAuthenticatedRpc` and cleared on disconnect / peer swap.
331
334
  * Only trusted while `authenticatedPeerId` still matches the bound
@@ -510,6 +513,10 @@ export class AiService extends TracedEventEmitter {
510
513
  super(config.tracer);
511
514
  this.config = config;
512
515
  setDefaultLogger(config.logger);
516
+ setPromptRequestMetricIdentity({
517
+ applicationId: config.applicationId,
518
+ organizationId: config.organizationId,
519
+ });
513
520
  // Store the wrapped logger so getLogger() can return it
514
521
  // The wrapped logger forwards logs to the browser console when Clark logs are enabled
515
522
  this._logger = getLogger();
@@ -923,6 +930,67 @@ export class AiService extends TracedEventEmitter {
923
930
  // when Clark logs are enabled (via ?clark_logs=true or SUPERBLOCKS_CLI_ENV=local)
924
931
  return this._logger;
925
932
  }
933
+ startGenerationTrace(request, promptId) {
934
+ // A span still open here means the previous run never reached a terminal
935
+ // transition. Close it now so its listeners cannot fire on this run's
936
+ // events.
937
+ this.activeGenerationTrace?.finish();
938
+ const parentContext = otelContext.active();
939
+ const span = this.config.tracer.startSpan("AiService.generation", {
940
+ attributes: {
941
+ "ai.mode": request.mode,
942
+ "ai.model": request.llmConfig?.coreModel,
943
+ "ai.provider": request.llmConfig?.provider,
944
+ "app.id": this.config.applicationId,
945
+ "org.id": this.config.organizationId,
946
+ },
947
+ kind: SpanKind.INTERNAL,
948
+ }, parentContext);
949
+ const spanContext = trace.setSpan(parentContext, span);
950
+ const traceHeaders = {};
951
+ propagation.inject(spanContext, traceHeaders);
952
+ let finished = false;
953
+ const finish = (error) => {
954
+ if (finished)
955
+ return;
956
+ finished = true;
957
+ this.off("generationCompleted", onCompleted);
958
+ this.off("generationFailed", onFailed);
959
+ if (this.activeGenerationTrace === generationTrace) {
960
+ this.activeGenerationTrace = undefined;
961
+ }
962
+ if (error === undefined) {
963
+ span.setStatus({ code: SpanStatusCode.OK });
964
+ }
965
+ else {
966
+ setSpanError(span, error);
967
+ }
968
+ span.end();
969
+ };
970
+ // Only end this span for its own run: generationCompleted carries the
971
+ // generation id, so a stale listener cannot close a later run's span.
972
+ // generationFailed carries no id, but a stale listener cannot survive
973
+ // into the next run either way — the span is finished (removing both
974
+ // listeners) when the FSM parks and, as a backstop, when the next run
975
+ // starts.
976
+ const onCompleted = (_producedDraft, generationId) => {
977
+ if (generationId === promptId) {
978
+ finish();
979
+ }
980
+ };
981
+ const onFailed = (error) => finish(error);
982
+ this.on("generationCompleted", onCompleted);
983
+ this.on("generationFailed", onFailed);
984
+ const generationTrace = {
985
+ context: spanContext,
986
+ finish,
987
+ traceHeaders: traceHeaders.traceparent || traceHeaders.tracestate
988
+ ? traceHeaders
989
+ : undefined,
990
+ };
991
+ this.activeGenerationTrace = generationTrace;
992
+ return generationTrace;
993
+ }
926
994
  createClarkTransitionHandler() {
927
995
  const clarkStateHandlerFactories = {
928
996
  Idle: doIdle,
@@ -960,7 +1028,7 @@ export class AiService extends TracedEventEmitter {
960
1028
  integrationStore: this.integrationStore,
961
1029
  knowledgeManager: this.knowledgeManager,
962
1030
  appContextStore: this.appContext,
963
- llmProvider: createLLMProvider(this.getLLMProviderSettings(clark.context.llmConfig), () => this.requireInferenceJwt(), clark.context.llmConfig, () => this.getLlmAiAttributes(clark)),
1031
+ llmProvider: createLLMProvider(this.getLLMProviderSettings(clark.context.llmConfig), () => this.requireInferenceJwt(), clark.context.llmConfig, () => this.getLlmAiAttributes(clark), clark.context.traceHeaders),
964
1032
  chatSessionStore: this.chatSessionStore,
965
1033
  clarkProfiler: this.clarkProfiler,
966
1034
  contextManagerV2: this.contextManagerV2,
@@ -1021,12 +1089,16 @@ export class AiService extends TracedEventEmitter {
1021
1089
  if (clark.context.abortController?.signal.aborted &&
1022
1090
  event.type !== USER_CANCELED &&
1023
1091
  event.type !== AGENT_CANCELED) {
1024
- return clark.send({ type: USER_CANCELED });
1092
+ return clark.send({
1093
+ type: USER_CANCELED,
1094
+ failure: promptFailureFromStreamAbort(clark.context),
1095
+ });
1025
1096
  }
1026
1097
  if (event.type !== AGENT_CANCELED) {
1027
1098
  return clark.send({
1028
1099
  type: AGENT_CANCELED,
1029
1100
  advice: `An unexpected error occurred. Please try again.`,
1101
+ failure: promptFailurePlatform("handler_error"),
1030
1102
  });
1031
1103
  }
1032
1104
  throw error;
@@ -1134,7 +1206,7 @@ export class AiService extends TracedEventEmitter {
1134
1206
  return selectContextManagementStrategy({
1135
1207
  serverContextManagementEnabled: llmConfig?.serverContextManagement?.enabled,
1136
1208
  thresholds: llmConfig?.serverContextManagement,
1137
- summarizationModel: () => createLLMProvider(this.getLLMProviderSettings(llmConfig), () => this.getInferenceJwt(), llmConfig, () => this.getLlmAiAttributes(this.clark)).modelForTask("summarizeMessages").model,
1209
+ summarizationModel: () => createLLMProvider(this.getLLMProviderSettings(llmConfig), () => this.getInferenceJwt(), llmConfig, () => this.getLlmAiAttributes(this.clark), this.clark.context.traceHeaders).modelForTask("summarizeMessages").model,
1138
1210
  });
1139
1211
  }
1140
1212
  getLlmAiAttributes(clark) {
@@ -1384,14 +1456,14 @@ export class AiService extends TracedEventEmitter {
1384
1456
  async tryHandlePaywallForRequest(request) {
1385
1457
  try {
1386
1458
  await this.ensureAiQuotaAvailable(request.jwt);
1387
- return false;
1459
+ return undefined;
1388
1460
  }
1389
1461
  catch (error) {
1390
1462
  if (error instanceof Error) {
1391
1463
  const reason = getAiQuotaPaywallReasonFromMessage(error.message);
1392
1464
  if (reason) {
1393
1465
  await this.handlePreCallPaywall(request, reason);
1394
- return true;
1466
+ return reason;
1395
1467
  }
1396
1468
  }
1397
1469
  throw error;
@@ -1666,6 +1738,10 @@ export class AiService extends TracedEventEmitter {
1666
1738
  async handleAiGenerate(request) {
1667
1739
  this.getLogger().info(`[ai-service] handleAiGenerate: peerId=${this.stablePeer.peerId ?? "unknown"}`);
1668
1740
  if (this.clark.state === ClarkStateNames.Dead) {
1741
+ recordPromptEntryRejection({
1742
+ dedupeKey: resolvePromptRequestDedupeKey(request),
1743
+ failure: promptFailurePlatform("service_dead"),
1744
+ });
1669
1745
  throw new Error("Service is unavailable");
1670
1746
  }
1671
1747
  // a reconnect can cause the client to re-send this prompt
@@ -1676,6 +1752,10 @@ export class AiService extends TracedEventEmitter {
1676
1752
  this.assertNoActivePolicyGateRun();
1677
1753
  if (this.isBusy()) {
1678
1754
  this.getLogger().warn("[ai-service] Service is busy. State:", this.clark.state);
1755
+ recordPromptEntryRejection({
1756
+ dedupeKey: resolvePromptRequestDedupeKey(request),
1757
+ failure: promptFailurePlatform("service_busy"),
1758
+ });
1679
1759
  throw new Error("Service is busy");
1680
1760
  }
1681
1761
  // Pre-call quota check. When denied, `ensureAiQuotaAvailable` throws so
@@ -1688,7 +1768,12 @@ export class AiService extends TracedEventEmitter {
1688
1768
  // advice + open modal via the same `aiSetGenerationState(CANCELLED,
1689
1769
  // paywallReason)` saga path the mid-stream paywall uses, then resolve
1690
1770
  // the socket call so the UI's catch doesn't run. (APPS-4080)
1691
- if (await this.tryHandlePaywallForRequest(request)) {
1771
+ const paywallReason = await this.tryHandlePaywallForRequest(request);
1772
+ if (paywallReason) {
1773
+ recordPromptEntryRejection({
1774
+ dedupeKey: resolvePromptRequestDedupeKey(request),
1775
+ failure: promptFailurePaywall(paywallReason),
1776
+ });
1692
1777
  return;
1693
1778
  }
1694
1779
  if (request.requestId &&
@@ -1696,11 +1781,18 @@ export class AiService extends TracedEventEmitter {
1696
1781
  this.getLogger().warn("[ai-service] Ignoring duplicate aiGenerate re-send (lost dedup race)", { requestId: request.requestId });
1697
1782
  return;
1698
1783
  }
1784
+ recordPromptAttempt(resolvePromptRequestDedupeKey(request));
1699
1785
  // The id is now recorded. If anything below throws before generation is
1700
1786
  // actually kicked off (the `transitionTo` USER_SENT_PROMPT), evict it so a
1701
1787
  // legitimate reconnect re-send of this same prompt is not swallowed as a
1702
1788
  // duplicate while nothing is running. (APPS-4663)
1789
+ let generationTrace;
1703
1790
  try {
1791
+ // Generate prompt_id for this user interaction. It doubles as the
1792
+ // generation id on terminal events, so the trace needs it up front.
1793
+ const promptId = randomUUID();
1794
+ generationTrace = this.startGenerationTrace(request, promptId);
1795
+ const activeGenerationTrace = generationTrace;
1704
1796
  this.clarkProfiler.startSession();
1705
1797
  getLogger().info("AI Service received request with llmConfig:", request.llmConfig || {});
1706
1798
  getLogger().info("[ai-service] handleAiGenerate received request", {
@@ -1710,16 +1802,20 @@ export class AiService extends TracedEventEmitter {
1710
1802
  existingPlanContextApproved: this.clark.context.planContext?.approved,
1711
1803
  responseMetadataType: request.responseMetadata?.type,
1712
1804
  });
1713
- // Generate prompt_id for this user interaction
1714
- const promptId = randomUUID();
1805
+ const promptOutcomeDedupeKey = resolvePromptRequestDedupeKey(request);
1715
1806
  getLogger().info("[ai-service] Generated prompt_id for user interaction", {
1716
1807
  promptId,
1808
+ requestId: request.requestId,
1809
+ dedupeKeySource: request.requestId ? "requestId" : "ephemeral",
1717
1810
  applicationId: this.config.applicationId,
1718
1811
  });
1719
1812
  this.clark.updateContext((context) => {
1720
1813
  return {
1721
1814
  ...context,
1722
1815
  promptId,
1816
+ promptOutcomeDedupeKey,
1817
+ isInterruptAbort: undefined,
1818
+ userInitiatedPromptAbort: undefined,
1723
1819
  // Third-party import turns are billed as app_migration; everything
1724
1820
  // else falls back to the default in-app Clark generation metadata.
1725
1821
  usageMetadata: request.importSource
@@ -1740,20 +1836,42 @@ export class AiService extends TracedEventEmitter {
1740
1836
  workflowStartedAt: new Date().toISOString(),
1741
1837
  },
1742
1838
  llmConfig: request.llmConfig,
1839
+ traceHeaders: activeGenerationTrace.traceHeaders,
1743
1840
  partialResponse: undefined,
1744
1841
  userPreferences: request.userPreferences,
1745
1842
  };
1746
1843
  });
1747
1844
  const transitionTo = transitionFrom(this.clark);
1748
- void transitionTo({
1845
+ void otelContext
1846
+ .with(activeGenerationTrace.context, () => Promise.resolve(transitionTo({
1749
1847
  type: USER_SENT_PROMPT,
1750
1848
  request,
1849
+ })))
1850
+ .catch((error) => {
1851
+ activeGenerationTrace.finish(error);
1852
+ if (request.requestId) {
1853
+ this.requestDeduplicator.forget(request.requestId);
1854
+ }
1855
+ recordPromptOutcome({
1856
+ dedupeKey: promptOutcomeDedupeKey,
1857
+ outcome: "failure",
1858
+ failure: promptFailurePlatform("handler_error"),
1859
+ });
1860
+ this.getLogger().error("[ai-service] Failed to start generation", {
1861
+ error,
1862
+ });
1751
1863
  });
1752
1864
  }
1753
1865
  catch (error) {
1866
+ generationTrace?.finish(error);
1754
1867
  if (request.requestId) {
1755
1868
  this.requestDeduplicator.forget(request.requestId);
1756
1869
  }
1870
+ recordPromptOutcome({
1871
+ dedupeKey: resolvePromptRequestDedupeKey(request),
1872
+ outcome: "failure",
1873
+ failure: promptFailurePlatform("handler_error"),
1874
+ });
1757
1875
  throw error;
1758
1876
  }
1759
1877
  }
@@ -2049,7 +2167,7 @@ export class AiService extends TracedEventEmitter {
2049
2167
  application_id: this.config.applicationId,
2050
2168
  session_id: this.sessionId,
2051
2169
  }));
2052
- const { model } = llmProvider.modelForTask("fast");
2170
+ const { model, providerConfig } = llmProvider.modelForTask("fast");
2053
2171
  const LabelAttachmentSchema = z.object({
2054
2172
  label: z
2055
2173
  .string()
@@ -2065,21 +2183,31 @@ export class AiService extends TracedEventEmitter {
2065
2183
  try {
2066
2184
  // Build the appropriate content part based on attachment type
2067
2185
  const contentPart = this.attachmentToContentPart(attachment);
2068
- const { object: result } = await generateObject({
2186
+ // Route labeling through the shared LLMClient (APPS-5061) as a stateless
2187
+ // one-shot, mirroring the APPS-1213 (#20263) migrations. No safety config:
2188
+ // labeling had no safety pass before this migration, and redacting the
2189
+ // upload before the model would defeat labeling (the model must see it).
2190
+ // Upload content can still carry secrets, so we keep it out of telemetry
2191
+ // via recordObsContent: false rather than via a safety pass.
2192
+ const result = await this.llmClient.generateObject({
2069
2193
  model,
2070
2194
  schema: LabelAttachmentSchema,
2071
- messages: [
2072
- {
2073
- role: "user",
2074
- content: [
2075
- {
2076
- type: "text",
2077
- text: "Analyze this attachment and provide a brief label and key insight about its content.",
2078
- },
2079
- contentPart,
2080
- ],
2081
- },
2082
- ],
2195
+ context: { mode: "stateless" },
2196
+ // Raw user data may carry secrets; keep it out of LLMObs span content
2197
+ // (metrics/model/timing are still recorded).
2198
+ recordObsContent: false,
2199
+ user: {
2200
+ role: "user",
2201
+ content: [
2202
+ {
2203
+ type: "text",
2204
+ text: "Analyze this attachment and provide a brief label and key insight about its content.",
2205
+ },
2206
+ contentPart,
2207
+ ],
2208
+ },
2209
+ providerConfig,
2210
+ retry: { maxAttempts: 2 },
2083
2211
  abortSignal: timeoutController.signal,
2084
2212
  });
2085
2213
  const durationMs = Date.now() - startTime;
@@ -2207,17 +2335,44 @@ export class AiService extends TracedEventEmitter {
2207
2335
  await this.clark.recordSystemMessage(params, "Environment event", "User closed the integration setup form. The integration setup might not have been completed successfully.");
2208
2336
  }
2209
2337
  async handleUserCanceled() {
2210
- if (!this.isBusy()) {
2338
+ const awaitingUser = this.isAwaitingUser();
2339
+ if (!this.isBusy() && !awaitingUser) {
2211
2340
  return;
2212
2341
  }
2342
+ this.clark.updateContext((context) => ({
2343
+ ...context,
2344
+ userInitiatedPromptAbort: true,
2345
+ }));
2213
2346
  this.clark.context.abortController?.abort();
2347
+ if (this.isBusy()) {
2348
+ recordPromptOutcome({
2349
+ dedupeKey: resolvePromptOutcomeDedupeKey(this.clark.context),
2350
+ outcome: "failure",
2351
+ failure: promptFailureUserCanceled(),
2352
+ });
2353
+ }
2354
+ if (awaitingUser && this.clark.context.awaitingUserHandoffRecorded) {
2355
+ recordAwaitingUserHandoffDismissed({
2356
+ awaitingUserHandoffPromptId: this.clark.context.awaitingUserHandoffPromptId,
2357
+ awaitingUserHandoffRecorded: this.clark.context.awaitingUserHandoffRecorded,
2358
+ promptId: this.clark.context.promptId,
2359
+ promptOutcomeDedupeKey: this.clark.context.promptOutcomeDedupeKey,
2360
+ reason: "cancel",
2361
+ });
2362
+ }
2214
2363
  // Clear the prompt_id since this prompt was abandoned
2215
2364
  this.clark.updateContext((context) => ({
2216
2365
  ...context,
2217
2366
  promptId: undefined,
2367
+ promptOutcomeDedupeKey: undefined,
2218
2368
  usageMetadata: undefined,
2369
+ awaitingUserHandoffPromptId: undefined,
2370
+ awaitingUserHandoffRecorded: undefined,
2219
2371
  }));
2220
- void this.clark.send({ type: USER_CANCELED });
2372
+ void this.clark.send({
2373
+ type: USER_CANCELED,
2374
+ failure: promptFailureUserCanceled(),
2375
+ });
2221
2376
  }
2222
2377
  /**
2223
2378
  * UI-initiated cancellation of a background task. Delegates to
@@ -2268,11 +2423,16 @@ export class AiService extends TracedEventEmitter {
2268
2423
  async handleAiGenerateWithQueue(request, mode) {
2269
2424
  this.getLogger().info(`[ai-service] handleAiGenerateWithQueue: peerId=${this.stablePeer.peerId ?? "unknown"}, mode=${mode}`);
2270
2425
  if (this.clark.state === ClarkStateNames.Dead) {
2426
+ recordPromptEntryRejection({
2427
+ dedupeKey: resolvePromptRequestDedupeKey(request),
2428
+ failure: promptFailurePlatform("service_dead"),
2429
+ });
2271
2430
  throw new Error("Service is unavailable");
2272
2431
  }
2273
2432
  // peek (do not record) so a re-sent prompt is dropped before the
2274
- // interrupt path tears down the in-flight generation. `handleAiGenerate` is
2275
- // the single recording point and stamps the id once this proceeds.
2433
+ // interrupt path tears down the in-flight generation. Entry metrics
2434
+ // (attempt + pre-call paywall) are recorded in handleAiGenerate or
2435
+ // interruptAndContinue once this proceeds.
2276
2436
  if (request.requestId && this.requestDeduplicator.has(request.requestId)) {
2277
2437
  this.getLogger().info("[ai-service] Ignoring duplicate aiGenerateWithQueue re-send", { requestId: request.requestId, mode });
2278
2438
  return { status: "started" };
@@ -2306,7 +2466,10 @@ export class AiService extends TracedEventEmitter {
2306
2466
  availableIntegrations: this.clark.context.availableIntegrations,
2307
2467
  };
2308
2468
  this.getLogger().info(`[ai-service] Interrupting current generation. Partial response length: ${partialResponse.length}`);
2309
- this.clark.updateContext({ isInterruptAbort: true });
2469
+ this.clark.updateContext({
2470
+ isInterruptAbort: true,
2471
+ userInitiatedPromptAbort: true,
2472
+ });
2310
2473
  this.clark.context.abortController?.abort();
2311
2474
  await this.waitForAbortComplete();
2312
2475
  // Note: `interruptedPreviousGeneration` and `interruptOriginalPrompt`
@@ -2322,6 +2485,8 @@ export class AiService extends TracedEventEmitter {
2322
2485
  availableIntegrations: preservedContext.availableIntegrations,
2323
2486
  abortController: new AbortController(),
2324
2487
  partialResponse: undefined,
2488
+ isInterruptAbort: undefined,
2489
+ userInitiatedPromptAbort: undefined,
2325
2490
  });
2326
2491
  if (partialResponse.trim().length > 10) {
2327
2492
  await this.chatSessionStore.recordAssistant({
@@ -2335,7 +2500,12 @@ export class AiService extends TracedEventEmitter {
2335
2500
  // `handleAiGenerate`. The aborted run is NOT resumed; the user can
2336
2501
  // start fresh once credits are restored via the same "send any
2337
2502
  // message" affordance the standard paywall uses.
2338
- if (await this.tryHandlePaywallForRequest(request)) {
2503
+ const paywallReason = await this.tryHandlePaywallForRequest(request);
2504
+ if (paywallReason) {
2505
+ recordPromptEntryRejection({
2506
+ dedupeKey: resolvePromptRequestDedupeKey(request),
2507
+ failure: promptFailurePaywall(paywallReason),
2508
+ });
2339
2509
  return;
2340
2510
  }
2341
2511
  // Only set the interrupt-state fields once we're committed to calling
@@ -3636,6 +3806,11 @@ export class AiService extends TracedEventEmitter {
3636
3806
  * Should be called when Clark transitions to Idle or AwaitingUser state.
3637
3807
  */
3638
3808
  triggerGenerationComplete() {
3809
+ // The FSM parked, so any still-open generation span is over. Runs that
3810
+ // completed or failed already ended it via the generation events; this
3811
+ // ends it on the paths that emit neither (user/agent cancel,
3812
+ // runtime-updated-without-edits, planning errors that park in Idle).
3813
+ this.activeGenerationTrace?.finish();
3639
3814
  if (this._onGenerationCompleteCallback) {
3640
3815
  const callback = this._onGenerationCompleteCallback;
3641
3816
  this._onGenerationCompleteCallback = undefined;