@open-mercato/ai-assistant 0.9.1-develop.7532.1.8eac7e198f → 0.9.1-develop.7537.1.65452bc2a0

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.
@@ -229,6 +229,8 @@
229
229
  "ai_assistant.chat.mutation_cards.preview.handlerError": "Der Mutations-Handler meldete einen Fehler. Prüfen Sie die Details und schlagen Sie die Aktion ggf. erneut vor.",
230
230
  "ai_assistant.chat.mutation_cards.preview.reviewDetails": "Details ansehen",
231
231
  "ai_assistant.chat.mutation_cards.preview.title": "Vorgeschlagene Änderungen prüfen",
232
+ "ai_assistant.chat.mutation_cards.result.cancelledBody": "Sie haben diese vorgeschlagene Änderung abgebrochen. Es wurden keine Änderungen vorgenommen.",
233
+ "ai_assistant.chat.mutation_cards.result.cancelledTitle": "Vorschlag abgebrochen",
232
234
  "ai_assistant.chat.mutation_cards.result.failureBody": "Die Änderung konnte nicht angewendet werden.",
233
235
  "ai_assistant.chat.mutation_cards.result.failureTitle": "Aktion fehlgeschlagen",
234
236
  "ai_assistant.chat.mutation_cards.result.fixWithAi": "Mit KI korrigieren",
@@ -229,6 +229,8 @@
229
229
  "ai_assistant.chat.mutation_cards.preview.handlerError": "The mutation handler reported an error. Review the details and re-propose if needed.",
230
230
  "ai_assistant.chat.mutation_cards.preview.reviewDetails": "Review details",
231
231
  "ai_assistant.chat.mutation_cards.preview.title": "Review proposed changes",
232
+ "ai_assistant.chat.mutation_cards.result.cancelledBody": "You cancelled this proposed change. No changes were made.",
233
+ "ai_assistant.chat.mutation_cards.result.cancelledTitle": "Proposal cancelled",
232
234
  "ai_assistant.chat.mutation_cards.result.failureBody": "The mutation could not be applied.",
233
235
  "ai_assistant.chat.mutation_cards.result.failureTitle": "Action failed",
234
236
  "ai_assistant.chat.mutation_cards.result.fixWithAi": "Fix with AI",
@@ -229,6 +229,8 @@
229
229
  "ai_assistant.chat.mutation_cards.preview.handlerError": "El gestor de mutaciones informó un error. Revisa los detalles y vuelve a proponer la acción si es necesario.",
230
230
  "ai_assistant.chat.mutation_cards.preview.reviewDetails": "Ver detalles",
231
231
  "ai_assistant.chat.mutation_cards.preview.title": "Revisar los cambios propuestos",
232
+ "ai_assistant.chat.mutation_cards.result.cancelledBody": "Cancelaste este cambio propuesto. No se realizó ningún cambio.",
233
+ "ai_assistant.chat.mutation_cards.result.cancelledTitle": "Propuesta cancelada",
232
234
  "ai_assistant.chat.mutation_cards.result.failureBody": "No se pudo aplicar la mutación.",
233
235
  "ai_assistant.chat.mutation_cards.result.failureTitle": "La acción falló",
234
236
  "ai_assistant.chat.mutation_cards.result.fixWithAi": "Corregir con IA",
@@ -229,6 +229,8 @@
229
229
  "ai_assistant.chat.mutation_cards.preview.handlerError": "뮤테이션 핸들러가 오류를 보고했습니다. 세부 정보를 검토하고 필요하면 다시 제안하세요.",
230
230
  "ai_assistant.chat.mutation_cards.preview.reviewDetails": "세부 정보 검토",
231
231
  "ai_assistant.chat.mutation_cards.preview.title": "제안된 변경 사항 검토",
232
+ "ai_assistant.chat.mutation_cards.result.cancelledBody": "이 제안된 변경을 취소했습니다. 변경 사항이 적용되지 않았습니다.",
233
+ "ai_assistant.chat.mutation_cards.result.cancelledTitle": "제안이 취소됨",
232
234
  "ai_assistant.chat.mutation_cards.result.failureBody": "뮤테이션을 적용할 수 없습니다.",
233
235
  "ai_assistant.chat.mutation_cards.result.failureTitle": "작업 실패",
234
236
  "ai_assistant.chat.mutation_cards.result.fixWithAi": "AI로 수정",
@@ -229,6 +229,8 @@
229
229
  "ai_assistant.chat.mutation_cards.preview.handlerError": "Handler mutacji zgłosił błąd. Sprawdź szczegóły i w razie potrzeby zaproponuj akcję ponownie.",
230
230
  "ai_assistant.chat.mutation_cards.preview.reviewDetails": "Pokaż szczegóły",
231
231
  "ai_assistant.chat.mutation_cards.preview.title": "Sprawdź proponowane zmiany",
232
+ "ai_assistant.chat.mutation_cards.result.cancelledBody": "Anulowano tę proponowaną zmianę. Nie wprowadzono żadnych zmian.",
233
+ "ai_assistant.chat.mutation_cards.result.cancelledTitle": "Propozycja anulowana",
232
234
  "ai_assistant.chat.mutation_cards.result.failureBody": "Nie udało się zastosować zmiany.",
233
235
  "ai_assistant.chat.mutation_cards.result.failureTitle": "Akcja nie powiodła się",
234
236
  "ai_assistant.chat.mutation_cards.result.fixWithAi": "Popraw z AI",
@@ -54,7 +54,11 @@ async function executePendingActionCancel(input) {
54
54
  const executionResult = {
55
55
  error: {
56
56
  code: "cancelled_by_user",
57
- message: trimmedReason.length > 0 ? trimmedReason : "Cancelled by user"
57
+ // Left empty when no reason is supplied: this is surfaced as a
58
+ // neutral "proposal cancelled" message in the client, which
59
+ // supplies its own localized default copy (see MutationResultCard)
60
+ // instead of a hardcoded English string.
61
+ message: trimmedReason
58
62
  }
59
63
  };
60
64
  const cancelledRow = await repo.setStatus(action.id, "cancelled", scope, {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/ai_assistant/lib/pending-action-cancel.ts"],
4
- "sourcesContent": ["/**\n * Pending-action cancel executor (spec \u00A79.4, Step 5.9).\n *\n * Flips an `AiPendingAction` from `pending \u2192 cancelled` and emits the\n * typed `ai.action.cancelled` event via `emitAiAssistantEvent`. Unlike\n * {@link executePendingActionConfirm} the tool handler is NEVER invoked \u2014\n * cancellation is a pure state-machine transition plus an event emission.\n * Any other status short-circuits: already-`cancelled` is idempotent (no\n * second emit, same row returned); `confirmed` / `executing` / `failed`\n * are treated as invariant violations and bubble up as 409 via the route.\n *\n * If the row's `expiresAt` is in the past at the time of this call we\n * flip it to `expired` (not `cancelled`) \u2014 the Step 5.12 cleanup worker\n * races with this code path and we want a single canonical terminal\n * status for a row that reached its TTL.\n *\n * Idempotency: the caller MUST handle the already-`cancelled` branch\n * BEFORE invoking this helper so the event is emitted exactly once per\n * cancellation.\n */\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { AiPendingActionRepository } from '../data/repositories/AiPendingActionRepository'\nimport type { AiPendingAction } from '../data/entities'\nimport { emitAiAssistantEvent } from '../events'\nimport type {\n AiActionCancelledPayload,\n AiActionExpiredPayload,\n AiAssistantEventId,\n} from '../events'\nimport type { AiPendingActionExecutionResult } from './pending-action-types'\n\nconst logger = createLogger('ai_assistant')\n\nexport interface PendingActionCancelContext {\n tenantId: string\n organizationId: string | null\n userId: string\n container: import('awilix').AwilixContainer\n}\n\nexport type CancelEmitter = (\n eventId: Extract<AiAssistantEventId, 'ai.action.cancelled' | 'ai.action.expired'>,\n payload: AiActionCancelledPayload | AiActionExpiredPayload,\n) => Promise<void>\n\nexport interface PendingActionCancelInput {\n action: AiPendingAction\n ctx: PendingActionCancelContext\n /** Optional, caller-supplied cancellation reason (already trimmed by the route). */\n reason?: string | null\n repo?: AiPendingActionRepository\n /**\n * Injection seam for unit tests. When omitted, emission is routed via\n * the typed `emitAiAssistantEvent` helper (the normal production path).\n */\n emitEvent?: CancelEmitter\n now?: Date\n}\n\nexport type PendingActionCancelStatus = 'cancelled' | 'expired'\n\nexport interface PendingActionCancelResult {\n row: AiPendingAction\n status: PendingActionCancelStatus\n}\n\nconst CANCELLED_EVENT_ID = 'ai.action.cancelled' as const\nconst EXPIRED_EVENT_ID = 'ai.action.expired' as const\n\nconst defaultCancelEmitter: CancelEmitter = async (eventId, payload) => {\n await emitAiAssistantEvent(eventId, payload as unknown as Record<string, unknown>, {\n persistent: true,\n })\n}\n\nasync function emitEventSafe(\n emitter: CancelEmitter,\n eventId: Parameters<CancelEmitter>[0],\n payload: Parameters<CancelEmitter>[1],\n): Promise<void> {\n try {\n await emitter(eventId, payload)\n } catch (error) {\n logger.warn('Failed to emit pending-action event', { eventId, err: error })\n }\n}\n\n/**\n * Atomic `pending \u2192 cancelled` transition with TTL-race safety.\n *\n * - If `action.status === 'cancelled'`, returns the current row WITHOUT\n * emitting a second event. Callers should typically short-circuit on\n * this branch BEFORE invoking the helper (see the Step 5.9 route).\n * - If `action.expiresAt <= now`, the row is flipped to `expired` and the\n * `ai.action.expired` event is emitted via the typed\n * `emitAiAssistantEvent` helper (see `../events`). Returns\n * `{ status: 'expired' }` so the route can translate to a 409\n * `expired` envelope.\n * - Otherwise flips `pending \u2192 cancelled`, writes `resolvedAt` + the\n * optional cancellation reason onto `executionResult.error`, and emits\n * `ai.action.cancelled`.\n *\n * Any status other than `pending` / `cancelled` is treated as an\n * invariant violation \u2014 the route returns 409 `invalid_status` before\n * reaching this helper. If the caller invokes the helper on such a row\n * it will throw via the repo's state-machine guard.\n */\nexport async function executePendingActionCancel(\n input: PendingActionCancelInput,\n): Promise<PendingActionCancelResult> {\n const { action, ctx, now } = input\n const repo = input.repo ?? new AiPendingActionRepository(ctx.container.resolve('em'))\n const scope = {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n userId: ctx.userId,\n }\n const clock = now ?? new Date()\n const emitter: CancelEmitter = input.emitEvent ?? defaultCancelEmitter\n\n if (action.status === 'cancelled') {\n return { row: action, status: 'cancelled' }\n }\n\n const expiresAt =\n action.expiresAt instanceof Date ? action.expiresAt : new Date(action.expiresAt)\n if (expiresAt.getTime() <= clock.getTime()) {\n const expiredRow = await repo.setStatus(action.id, 'expired', scope, { now: clock })\n const resolvedAtIso =\n (expiredRow.resolvedAt ?? clock).toISOString?.() ?? new Date(clock).toISOString()\n const expiresAtIso =\n (expiresAt ?? clock).toISOString?.() ?? new Date(clock).toISOString()\n const expiredPayload: AiActionExpiredPayload = {\n pendingActionId: expiredRow.id,\n agentId: expiredRow.agentId,\n toolName: expiredRow.toolName,\n status: expiredRow.status,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n userId: ctx.userId,\n resolvedByUserId: null,\n resolvedAt: resolvedAtIso,\n expiresAt: expiresAtIso,\n expiredAt: resolvedAtIso,\n }\n await emitEventSafe(emitter, EXPIRED_EVENT_ID, expiredPayload)\n return { row: expiredRow, status: 'expired' }\n }\n\n const trimmedReason = typeof input.reason === 'string' ? input.reason.trim() : ''\n const executionResult: AiPendingActionExecutionResult = {\n error: {\n code: 'cancelled_by_user',\n message: trimmedReason.length > 0 ? trimmedReason : 'Cancelled by user',\n },\n }\n\n const cancelledRow = await repo.setStatus(action.id, 'cancelled', scope, {\n resolvedByUserId: ctx.userId,\n executionResult,\n now: clock,\n })\n const cancelledPayload: AiActionCancelledPayload = {\n pendingActionId: cancelledRow.id,\n agentId: cancelledRow.agentId,\n toolName: cancelledRow.toolName,\n status: cancelledRow.status,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n userId: ctx.userId,\n resolvedByUserId: ctx.userId,\n resolvedAt: (cancelledRow.resolvedAt ?? clock).toISOString?.() ?? new Date(clock).toISOString(),\n executionResult,\n ...(trimmedReason.length > 0 ? { reason: trimmedReason } : {}),\n }\n await emitEventSafe(emitter, CANCELLED_EVENT_ID, cancelledPayload)\n\n return { row: cancelledRow, status: 'cancelled' }\n}\n\nexport const PENDING_ACTION_CANCELLED_EVENT_ID = CANCELLED_EVENT_ID\nexport const PENDING_ACTION_EXPIRED_EVENT_ID = EXPIRED_EVENT_ID\n"],
5
- "mappings": "AAoBA,SAAS,oBAAoB;AAC7B,SAAS,iCAAiC;AAE1C,SAAS,4BAA4B;AAQrC,MAAM,SAAS,aAAa,cAAc;AAmC1C,MAAM,qBAAqB;AAC3B,MAAM,mBAAmB;AAEzB,MAAM,uBAAsC,OAAO,SAAS,YAAY;AACtE,QAAM,qBAAqB,SAAS,SAA+C;AAAA,IACjF,YAAY;AAAA,EACd,CAAC;AACH;AAEA,eAAe,cACb,SACA,SACA,SACe;AACf,MAAI;AACF,UAAM,QAAQ,SAAS,OAAO;AAAA,EAChC,SAAS,OAAO;AACd,WAAO,KAAK,uCAAuC,EAAE,SAAS,KAAK,MAAM,CAAC;AAAA,EAC5E;AACF;AAsBA,eAAsB,2BACpB,OACoC;AACpC,QAAM,EAAE,QAAQ,KAAK,IAAI,IAAI;AAC7B,QAAM,OAAO,MAAM,QAAQ,IAAI,0BAA0B,IAAI,UAAU,QAAQ,IAAI,CAAC;AACpF,QAAM,QAAQ;AAAA,IACZ,UAAU,IAAI;AAAA,IACd,gBAAgB,IAAI;AAAA,IACpB,QAAQ,IAAI;AAAA,EACd;AACA,QAAM,QAAQ,OAAO,oBAAI,KAAK;AAC9B,QAAM,UAAyB,MAAM,aAAa;AAElD,MAAI,OAAO,WAAW,aAAa;AACjC,WAAO,EAAE,KAAK,QAAQ,QAAQ,YAAY;AAAA,EAC5C;AAEA,QAAM,YACJ,OAAO,qBAAqB,OAAO,OAAO,YAAY,IAAI,KAAK,OAAO,SAAS;AACjF,MAAI,UAAU,QAAQ,KAAK,MAAM,QAAQ,GAAG;AAC1C,UAAM,aAAa,MAAM,KAAK,UAAU,OAAO,IAAI,WAAW,OAAO,EAAE,KAAK,MAAM,CAAC;AACnF,UAAM,iBACH,WAAW,cAAc,OAAO,cAAc,KAAK,IAAI,KAAK,KAAK,EAAE,YAAY;AAClF,UAAM,gBACH,aAAa,OAAO,cAAc,KAAK,IAAI,KAAK,KAAK,EAAE,YAAY;AACtE,UAAM,iBAAyC;AAAA,MAC7C,iBAAiB,WAAW;AAAA,MAC5B,SAAS,WAAW;AAAA,MACpB,UAAU,WAAW;AAAA,MACrB,QAAQ,WAAW;AAAA,MACnB,UAAU,IAAI;AAAA,MACd,gBAAgB,IAAI,kBAAkB;AAAA,MACtC,QAAQ,IAAI;AAAA,MACZ,kBAAkB;AAAA,MAClB,YAAY;AAAA,MACZ,WAAW;AAAA,MACX,WAAW;AAAA,IACb;AACA,UAAM,cAAc,SAAS,kBAAkB,cAAc;AAC7D,WAAO,EAAE,KAAK,YAAY,QAAQ,UAAU;AAAA,EAC9C;AAEA,QAAM,gBAAgB,OAAO,MAAM,WAAW,WAAW,MAAM,OAAO,KAAK,IAAI;AAC/E,QAAM,kBAAkD;AAAA,IACtD,OAAO;AAAA,MACL,MAAM;AAAA,MACN,SAAS,cAAc,SAAS,IAAI,gBAAgB;AAAA,IACtD;AAAA,EACF;AAEA,QAAM,eAAe,MAAM,KAAK,UAAU,OAAO,IAAI,aAAa,OAAO;AAAA,IACvE,kBAAkB,IAAI;AAAA,IACtB;AAAA,IACA,KAAK;AAAA,EACP,CAAC;AACD,QAAM,mBAA6C;AAAA,IACjD,iBAAiB,aAAa;AAAA,IAC9B,SAAS,aAAa;AAAA,IACtB,UAAU,aAAa;AAAA,IACvB,QAAQ,aAAa;AAAA,IACrB,UAAU,IAAI;AAAA,IACd,gBAAgB,IAAI,kBAAkB;AAAA,IACtC,QAAQ,IAAI;AAAA,IACZ,kBAAkB,IAAI;AAAA,IACtB,aAAa,aAAa,cAAc,OAAO,cAAc,KAAK,IAAI,KAAK,KAAK,EAAE,YAAY;AAAA,IAC9F;AAAA,IACA,GAAI,cAAc,SAAS,IAAI,EAAE,QAAQ,cAAc,IAAI,CAAC;AAAA,EAC9D;AACA,QAAM,cAAc,SAAS,oBAAoB,gBAAgB;AAEjE,SAAO,EAAE,KAAK,cAAc,QAAQ,YAAY;AAClD;AAEO,MAAM,oCAAoC;AAC1C,MAAM,kCAAkC;",
4
+ "sourcesContent": ["/**\n * Pending-action cancel executor (spec \u00A79.4, Step 5.9).\n *\n * Flips an `AiPendingAction` from `pending \u2192 cancelled` and emits the\n * typed `ai.action.cancelled` event via `emitAiAssistantEvent`. Unlike\n * {@link executePendingActionConfirm} the tool handler is NEVER invoked \u2014\n * cancellation is a pure state-machine transition plus an event emission.\n * Any other status short-circuits: already-`cancelled` is idempotent (no\n * second emit, same row returned); `confirmed` / `executing` / `failed`\n * are treated as invariant violations and bubble up as 409 via the route.\n *\n * If the row's `expiresAt` is in the past at the time of this call we\n * flip it to `expired` (not `cancelled`) \u2014 the Step 5.12 cleanup worker\n * races with this code path and we want a single canonical terminal\n * status for a row that reached its TTL.\n *\n * Idempotency: the caller MUST handle the already-`cancelled` branch\n * BEFORE invoking this helper so the event is emitted exactly once per\n * cancellation.\n */\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { AiPendingActionRepository } from '../data/repositories/AiPendingActionRepository'\nimport type { AiPendingAction } from '../data/entities'\nimport { emitAiAssistantEvent } from '../events'\nimport type {\n AiActionCancelledPayload,\n AiActionExpiredPayload,\n AiAssistantEventId,\n} from '../events'\nimport type { AiPendingActionExecutionResult } from './pending-action-types'\n\nconst logger = createLogger('ai_assistant')\n\nexport interface PendingActionCancelContext {\n tenantId: string\n organizationId: string | null\n userId: string\n container: import('awilix').AwilixContainer\n}\n\nexport type CancelEmitter = (\n eventId: Extract<AiAssistantEventId, 'ai.action.cancelled' | 'ai.action.expired'>,\n payload: AiActionCancelledPayload | AiActionExpiredPayload,\n) => Promise<void>\n\nexport interface PendingActionCancelInput {\n action: AiPendingAction\n ctx: PendingActionCancelContext\n /** Optional, caller-supplied cancellation reason (already trimmed by the route). */\n reason?: string | null\n repo?: AiPendingActionRepository\n /**\n * Injection seam for unit tests. When omitted, emission is routed via\n * the typed `emitAiAssistantEvent` helper (the normal production path).\n */\n emitEvent?: CancelEmitter\n now?: Date\n}\n\nexport type PendingActionCancelStatus = 'cancelled' | 'expired'\n\nexport interface PendingActionCancelResult {\n row: AiPendingAction\n status: PendingActionCancelStatus\n}\n\nconst CANCELLED_EVENT_ID = 'ai.action.cancelled' as const\nconst EXPIRED_EVENT_ID = 'ai.action.expired' as const\n\nconst defaultCancelEmitter: CancelEmitter = async (eventId, payload) => {\n await emitAiAssistantEvent(eventId, payload as unknown as Record<string, unknown>, {\n persistent: true,\n })\n}\n\nasync function emitEventSafe(\n emitter: CancelEmitter,\n eventId: Parameters<CancelEmitter>[0],\n payload: Parameters<CancelEmitter>[1],\n): Promise<void> {\n try {\n await emitter(eventId, payload)\n } catch (error) {\n logger.warn('Failed to emit pending-action event', { eventId, err: error })\n }\n}\n\n/**\n * Atomic `pending \u2192 cancelled` transition with TTL-race safety.\n *\n * - If `action.status === 'cancelled'`, returns the current row WITHOUT\n * emitting a second event. Callers should typically short-circuit on\n * this branch BEFORE invoking the helper (see the Step 5.9 route).\n * - If `action.expiresAt <= now`, the row is flipped to `expired` and the\n * `ai.action.expired` event is emitted via the typed\n * `emitAiAssistantEvent` helper (see `../events`). Returns\n * `{ status: 'expired' }` so the route can translate to a 409\n * `expired` envelope.\n * - Otherwise flips `pending \u2192 cancelled`, writes `resolvedAt` + the\n * optional cancellation reason onto `executionResult.error`, and emits\n * `ai.action.cancelled`.\n *\n * Any status other than `pending` / `cancelled` is treated as an\n * invariant violation \u2014 the route returns 409 `invalid_status` before\n * reaching this helper. If the caller invokes the helper on such a row\n * it will throw via the repo's state-machine guard.\n */\nexport async function executePendingActionCancel(\n input: PendingActionCancelInput,\n): Promise<PendingActionCancelResult> {\n const { action, ctx, now } = input\n const repo = input.repo ?? new AiPendingActionRepository(ctx.container.resolve('em'))\n const scope = {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n userId: ctx.userId,\n }\n const clock = now ?? new Date()\n const emitter: CancelEmitter = input.emitEvent ?? defaultCancelEmitter\n\n if (action.status === 'cancelled') {\n return { row: action, status: 'cancelled' }\n }\n\n const expiresAt =\n action.expiresAt instanceof Date ? action.expiresAt : new Date(action.expiresAt)\n if (expiresAt.getTime() <= clock.getTime()) {\n const expiredRow = await repo.setStatus(action.id, 'expired', scope, { now: clock })\n const resolvedAtIso =\n (expiredRow.resolvedAt ?? clock).toISOString?.() ?? new Date(clock).toISOString()\n const expiresAtIso =\n (expiresAt ?? clock).toISOString?.() ?? new Date(clock).toISOString()\n const expiredPayload: AiActionExpiredPayload = {\n pendingActionId: expiredRow.id,\n agentId: expiredRow.agentId,\n toolName: expiredRow.toolName,\n status: expiredRow.status,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n userId: ctx.userId,\n resolvedByUserId: null,\n resolvedAt: resolvedAtIso,\n expiresAt: expiresAtIso,\n expiredAt: resolvedAtIso,\n }\n await emitEventSafe(emitter, EXPIRED_EVENT_ID, expiredPayload)\n return { row: expiredRow, status: 'expired' }\n }\n\n const trimmedReason = typeof input.reason === 'string' ? input.reason.trim() : ''\n const executionResult: AiPendingActionExecutionResult = {\n error: {\n code: 'cancelled_by_user',\n // Left empty when no reason is supplied: this is surfaced as a\n // neutral \"proposal cancelled\" message in the client, which\n // supplies its own localized default copy (see MutationResultCard)\n // instead of a hardcoded English string.\n message: trimmedReason,\n },\n }\n\n const cancelledRow = await repo.setStatus(action.id, 'cancelled', scope, {\n resolvedByUserId: ctx.userId,\n executionResult,\n now: clock,\n })\n const cancelledPayload: AiActionCancelledPayload = {\n pendingActionId: cancelledRow.id,\n agentId: cancelledRow.agentId,\n toolName: cancelledRow.toolName,\n status: cancelledRow.status,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n userId: ctx.userId,\n resolvedByUserId: ctx.userId,\n resolvedAt: (cancelledRow.resolvedAt ?? clock).toISOString?.() ?? new Date(clock).toISOString(),\n executionResult,\n ...(trimmedReason.length > 0 ? { reason: trimmedReason } : {}),\n }\n await emitEventSafe(emitter, CANCELLED_EVENT_ID, cancelledPayload)\n\n return { row: cancelledRow, status: 'cancelled' }\n}\n\nexport const PENDING_ACTION_CANCELLED_EVENT_ID = CANCELLED_EVENT_ID\nexport const PENDING_ACTION_EXPIRED_EVENT_ID = EXPIRED_EVENT_ID\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/ai-assistant",
3
- "version": "0.9.1-develop.7532.1.8eac7e198f",
3
+ "version": "0.9.1-develop.7537.1.65452bc2a0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "engines": {
@@ -100,16 +100,16 @@
100
100
  "zod-to-json-schema": "^3.25.2"
101
101
  },
102
102
  "peerDependencies": {
103
- "@open-mercato/shared": "0.9.1-develop.7532.1.8eac7e198f",
104
- "@open-mercato/ui": "0.9.1-develop.7532.1.8eac7e198f",
103
+ "@open-mercato/shared": "0.9.1-develop.7537.1.65452bc2a0",
104
+ "@open-mercato/ui": "0.9.1-develop.7537.1.65452bc2a0",
105
105
  "react": "^19.0.0",
106
106
  "react-dom": "^19.0.0",
107
107
  "zod": ">=3.23.0"
108
108
  },
109
109
  "devDependencies": {
110
- "@open-mercato/cli": "0.9.1-develop.7532.1.8eac7e198f",
111
- "@open-mercato/shared": "0.9.1-develop.7532.1.8eac7e198f",
112
- "@open-mercato/ui": "0.9.1-develop.7532.1.8eac7e198f",
110
+ "@open-mercato/cli": "0.9.1-develop.7537.1.65452bc2a0",
111
+ "@open-mercato/shared": "0.9.1-develop.7537.1.65452bc2a0",
112
+ "@open-mercato/ui": "0.9.1-develop.7537.1.65452bc2a0",
113
113
  "@types/react": "^19.2.18",
114
114
  "@types/react-dom": "^19.2.5",
115
115
  "react": "19.2.8",
@@ -250,14 +250,14 @@ describe('POST /api/ai/actions/:id/cancel route (Step 5.9)', () => {
250
250
  expect(repoGetByIdMock).not.toHaveBeenCalled()
251
251
  })
252
252
 
253
- it('whitespace-only reason becomes empty → default "Cancelled by user" message', async () => {
253
+ it('whitespace-only reason becomes empty (no hardcoded English copy)', async () => {
254
254
  repoGetByIdMock.mockResolvedValueOnce(makeRow())
255
255
 
256
256
  const response = await POST(buildRequest({ reason: ' \t\n ' }) as any, buildContext(PENDING_ACTION_ID))
257
257
  expect(response.status).toBe(200)
258
258
  const body = await response.json()
259
259
  expect(body.pendingAction.executionResult).toEqual({
260
- error: { code: 'cancelled_by_user', message: 'Cancelled by user' },
260
+ error: { code: 'cancelled_by_user', message: '' },
261
261
  })
262
262
  })
263
263
 
@@ -288,7 +288,7 @@ describe('POST /api/ai/actions/:id/cancel route (Step 5.9)', () => {
288
288
  const body = await response.json()
289
289
  expect(body.ok).toBe(true)
290
290
  expect(body.pendingAction.executionResult).toEqual({
291
- error: { code: 'cancelled_by_user', message: 'Cancelled by user' },
291
+ error: { code: 'cancelled_by_user', message: '' },
292
292
  })
293
293
  })
294
294
 
@@ -229,6 +229,8 @@
229
229
  "ai_assistant.chat.mutation_cards.preview.handlerError": "Der Mutations-Handler meldete einen Fehler. Prüfen Sie die Details und schlagen Sie die Aktion ggf. erneut vor.",
230
230
  "ai_assistant.chat.mutation_cards.preview.reviewDetails": "Details ansehen",
231
231
  "ai_assistant.chat.mutation_cards.preview.title": "Vorgeschlagene Änderungen prüfen",
232
+ "ai_assistant.chat.mutation_cards.result.cancelledBody": "Sie haben diese vorgeschlagene Änderung abgebrochen. Es wurden keine Änderungen vorgenommen.",
233
+ "ai_assistant.chat.mutation_cards.result.cancelledTitle": "Vorschlag abgebrochen",
232
234
  "ai_assistant.chat.mutation_cards.result.failureBody": "Die Änderung konnte nicht angewendet werden.",
233
235
  "ai_assistant.chat.mutation_cards.result.failureTitle": "Aktion fehlgeschlagen",
234
236
  "ai_assistant.chat.mutation_cards.result.fixWithAi": "Mit KI korrigieren",
@@ -229,6 +229,8 @@
229
229
  "ai_assistant.chat.mutation_cards.preview.handlerError": "The mutation handler reported an error. Review the details and re-propose if needed.",
230
230
  "ai_assistant.chat.mutation_cards.preview.reviewDetails": "Review details",
231
231
  "ai_assistant.chat.mutation_cards.preview.title": "Review proposed changes",
232
+ "ai_assistant.chat.mutation_cards.result.cancelledBody": "You cancelled this proposed change. No changes were made.",
233
+ "ai_assistant.chat.mutation_cards.result.cancelledTitle": "Proposal cancelled",
232
234
  "ai_assistant.chat.mutation_cards.result.failureBody": "The mutation could not be applied.",
233
235
  "ai_assistant.chat.mutation_cards.result.failureTitle": "Action failed",
234
236
  "ai_assistant.chat.mutation_cards.result.fixWithAi": "Fix with AI",
@@ -229,6 +229,8 @@
229
229
  "ai_assistant.chat.mutation_cards.preview.handlerError": "El gestor de mutaciones informó un error. Revisa los detalles y vuelve a proponer la acción si es necesario.",
230
230
  "ai_assistant.chat.mutation_cards.preview.reviewDetails": "Ver detalles",
231
231
  "ai_assistant.chat.mutation_cards.preview.title": "Revisar los cambios propuestos",
232
+ "ai_assistant.chat.mutation_cards.result.cancelledBody": "Cancelaste este cambio propuesto. No se realizó ningún cambio.",
233
+ "ai_assistant.chat.mutation_cards.result.cancelledTitle": "Propuesta cancelada",
232
234
  "ai_assistant.chat.mutation_cards.result.failureBody": "No se pudo aplicar la mutación.",
233
235
  "ai_assistant.chat.mutation_cards.result.failureTitle": "La acción falló",
234
236
  "ai_assistant.chat.mutation_cards.result.fixWithAi": "Corregir con IA",
@@ -229,6 +229,8 @@
229
229
  "ai_assistant.chat.mutation_cards.preview.handlerError": "뮤테이션 핸들러가 오류를 보고했습니다. 세부 정보를 검토하고 필요하면 다시 제안하세요.",
230
230
  "ai_assistant.chat.mutation_cards.preview.reviewDetails": "세부 정보 검토",
231
231
  "ai_assistant.chat.mutation_cards.preview.title": "제안된 변경 사항 검토",
232
+ "ai_assistant.chat.mutation_cards.result.cancelledBody": "이 제안된 변경을 취소했습니다. 변경 사항이 적용되지 않았습니다.",
233
+ "ai_assistant.chat.mutation_cards.result.cancelledTitle": "제안이 취소됨",
232
234
  "ai_assistant.chat.mutation_cards.result.failureBody": "뮤테이션을 적용할 수 없습니다.",
233
235
  "ai_assistant.chat.mutation_cards.result.failureTitle": "작업 실패",
234
236
  "ai_assistant.chat.mutation_cards.result.fixWithAi": "AI로 수정",
@@ -229,6 +229,8 @@
229
229
  "ai_assistant.chat.mutation_cards.preview.handlerError": "Handler mutacji zgłosił błąd. Sprawdź szczegóły i w razie potrzeby zaproponuj akcję ponownie.",
230
230
  "ai_assistant.chat.mutation_cards.preview.reviewDetails": "Pokaż szczegóły",
231
231
  "ai_assistant.chat.mutation_cards.preview.title": "Sprawdź proponowane zmiany",
232
+ "ai_assistant.chat.mutation_cards.result.cancelledBody": "Anulowano tę proponowaną zmianę. Nie wprowadzono żadnych zmian.",
233
+ "ai_assistant.chat.mutation_cards.result.cancelledTitle": "Propozycja anulowana",
232
234
  "ai_assistant.chat.mutation_cards.result.failureBody": "Nie udało się zastosować zmiany.",
233
235
  "ai_assistant.chat.mutation_cards.result.failureTitle": "Akcja nie powiodła się",
234
236
  "ai_assistant.chat.mutation_cards.result.fixWithAi": "Popraw z AI",
@@ -147,7 +147,7 @@ describe('executePendingActionCancel', () => {
147
147
  expect(typeof emittedPayload.resolvedAt).toBe('string')
148
148
  })
149
149
 
150
- it('defaults to "Cancelled by user" message when no reason is supplied', async () => {
150
+ it('stores an empty message when no reason is supplied (no hardcoded English copy)', async () => {
151
151
  const repo = makeRepoStub(makeAction())
152
152
  const emitEvent = jest.fn().mockResolvedValue(undefined)
153
153
  const clock = new Date('2026-04-18T10:05:00.000Z')
@@ -164,7 +164,7 @@ describe('executePendingActionCancel', () => {
164
164
  const [, , , extra] = repo.setStatus.mock.calls[0]
165
165
  expect(extra).toMatchObject({
166
166
  executionResult: {
167
- error: { code: 'cancelled_by_user', message: 'Cancelled by user' },
167
+ error: { code: 'cancelled_by_user', message: '' },
168
168
  },
169
169
  })
170
170
  const [, payload] = emitEvent.mock.calls[0] as [string, AiActionCancelledPayload]
@@ -151,7 +151,11 @@ export async function executePendingActionCancel(
151
151
  const executionResult: AiPendingActionExecutionResult = {
152
152
  error: {
153
153
  code: 'cancelled_by_user',
154
- message: trimmedReason.length > 0 ? trimmedReason : 'Cancelled by user',
154
+ // Left empty when no reason is supplied: this is surfaced as a
155
+ // neutral "proposal cancelled" message in the client, which
156
+ // supplies its own localized default copy (see MutationResultCard)
157
+ // instead of a hardcoded English string.
158
+ message: trimmedReason,
155
159
  },
156
160
  }
157
161