@ai-matrx/chat 0.7.6 → 0.7.8

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 (65) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/agents/api/fetch-pending-calls.js +4 -3
  3. package/dist/agents/api/submit-tool-results.js +3 -6
  4. package/dist/agents/components/chat/begin-fresh-chat.js +2 -2
  5. package/dist/agents/components/inputs/smart-input/InputActionButtons.d.ts +2 -2
  6. package/dist/agents/components/inputs/smart-input/InputActionButtons.js +3 -1
  7. package/dist/agents/components/inputs/smart-input/SmartAgentInputStacked.js +8 -3
  8. package/dist/agents/components/inputs/smart-input/composer/ComposerSplash.js +2 -3
  9. package/dist/agents/components/inputs/smart-input/composer/useComposerMode.js +4 -2
  10. package/dist/agents/components/live-run/LiveRunDisplay.d.ts +7 -1
  11. package/dist/agents/components/live-run/LiveRunDisplay.js +3 -1
  12. package/dist/agents/components/messages-display/assistant/AssistantMessageFooter.js +4 -2
  13. package/dist/agents/components/shared/transcript-layout.d.ts +9 -8
  14. package/dist/agents/components/shared/transcript-layout.js +2 -2
  15. package/dist/agents/redux/agent-definition/agent-cache-bust-request.js +7 -7
  16. package/dist/agents/redux/execution-system/context-rules/receipt-check.js +2 -2
  17. package/dist/agents/redux/execution-system/inbox/inbox.thunks.d.ts +2 -2
  18. package/dist/agents/redux/execution-system/inbox/inbox.thunks.js +7 -15
  19. package/dist/agents/redux/execution-system/observational-memory/fetch-memory-cost.thunk.d.ts +1 -1
  20. package/dist/agents/redux/execution-system/observational-memory/fetch-memory-cost.thunk.js +7 -4
  21. package/dist/agents/redux/execution-system/selectors/aggregate.selectors.d.ts +1 -114
  22. package/dist/agents/redux/execution-system/selectors/aggregate.selectors.js +28 -43
  23. package/dist/agents/redux/execution-system/thunks/call-conversation-api.d.ts +44 -0
  24. package/dist/agents/redux/execution-system/thunks/call-conversation-api.js +78 -0
  25. package/dist/agents/redux/execution-system/thunks/continue-agent-json.d.ts +41 -0
  26. package/dist/agents/redux/execution-system/thunks/continue-agent-json.js +66 -0
  27. package/dist/agents/redux/execution-system/thunks/execute-instance.thunk.d.ts +20 -0
  28. package/dist/agents/redux/execution-system/thunks/execute-instance.thunk.js +9 -2
  29. package/dist/agents/redux/execution-system/thunks/launch-agent-execution.thunk.js +4 -6
  30. package/dist/agents/redux/execution-system/thunks/notify-and-play-dispatcher.d.ts +1 -5
  31. package/dist/agents/redux/execution-system/thunks/notify-and-play-dispatcher.js +2 -1
  32. package/dist/agents/redux/execution-system/thunks/resolve-base-url.js +7 -17
  33. package/dist/agents/redux/execution-system/thunks/settle-after-stop.thunk.js +2 -2
  34. package/dist/agents/redux/execution-system/thunks/smart-execute.thunk.js +3 -3
  35. package/dist/agents/redux/execution-system/utils/continuation-body.d.ts +8 -2
  36. package/dist/agents/redux/execution-system/utils/continuation-body.js +3 -3
  37. package/dist/agents/redux/execution-system/utils/required-organization.d.ts +18 -0
  38. package/dist/agents/redux/execution-system/utils/required-organization.js +14 -10
  39. package/dist/agents/services/mcp-connections.service.js +5 -8
  40. package/dist/agents/ui-first-tools/dispatcher/dispatch-ui-first-tool.thunk.js +2 -1
  41. package/dist/host/defaults/files-host.js +11 -1
  42. package/dist/host/defaults/server-api.d.ts +1 -15
  43. package/dist/host/defaults/server-api.js +0 -21
  44. package/dist/host/defaults/server.d.ts +2 -1
  45. package/dist/host/defaults/server.js +8 -6
  46. package/dist/host/request-headers.d.ts +26 -0
  47. package/dist/host/request-headers.js +17 -0
  48. package/dist/host/server/call-api.d.ts +0 -13
  49. package/dist/host/server/call-api.js +0 -2
  50. package/dist/host/server/matrx-transport.d.ts +0 -1
  51. package/dist/host/server/matrx-transport.js +0 -2
  52. package/dist/store/root-state.d.ts +4 -21
  53. package/dist/surfaces/hooks/useSurfaceConfig.js +2 -1
  54. package/dist/surfaces/manifests/agent-builder.manifest.d.ts +8 -0
  55. package/dist/surfaces/manifests/agent-builder.manifest.js +62 -2
  56. package/dist/surfaces/runtime/surface-feedback.js +2 -2
  57. package/dist/ui/mandates-storage/mandateStorage.d.ts +36 -0
  58. package/dist/ui/mandates-storage/shortcutStorage.d.ts +9 -0
  59. package/dist/ui/markdown-stream/EnhancedChatMarkdown.js +22 -0
  60. package/dist/ui/markdown-stream/MarkdownStreamImpl.js +3 -3
  61. package/dist/ui/markdown-stream/StreamTextFallback.d.ts +6 -0
  62. package/dist/ui/markdown-stream/StreamTextFallback.js +23 -0
  63. package/dist/ui/markdown-stream/merge-thinking-across-hidden.d.ts +19 -0
  64. package/dist/ui/markdown-stream/merge-thinking-across-hidden.js +21 -0
  65. package/package.json +16 -11
@@ -11,6 +11,7 @@ import {
11
11
  selectShowAutoClearToggle
12
12
  } from "../instance-ui-state/instance-ui-state.selectors.js";
13
13
  import { assembleRequest } from "../thunks/execute-instance.thunk.js";
14
+ import { selectAppContext } from "../../../../context/sources/scopes.js";
14
15
  import {
15
16
  contextItemBindingOf,
16
17
  isCustomDataBinding
@@ -170,49 +171,33 @@ const selectIsInstanceReady = (conversationId) => {
170
171
  return selector;
171
172
  };
172
173
  function makeSelectAssembledRequest(conversationId) {
173
- return createSelector(
174
- [
175
- (s) => s.conversations,
176
- (s) => s.instanceUIState,
177
- (s) => s.instanceUserInput,
178
- (s) => s.instanceResources,
179
- (s) => s.instanceVariableValues,
180
- (s) => s.instanceModelOverrides,
181
- (s) => s.instanceContext,
182
- (s) => s.instanceClientTools,
183
- (s) => s.appContext,
184
- // Identity, active org and the person's preferences (P7/P8): the package's own host slice.
185
- (s) => s.chatHost,
186
- // The context door (buildRequestContext) reads these too: the agent's
187
- // policies + kill switch, the person's saved context rules, whether this
188
- // is the first turn, and who is signed in (first-turn system values).
189
- (s) => s.agentDefinition,
190
- (s) => s.surfaceUserState,
191
- (s) => s.messages,
192
- (s) => s.userAuth,
193
- (s) => s.userProfile
194
- ],
195
- (conversations, instanceUIState, instanceUserInput, instanceResources, instanceVariableValues, instanceModelOverrides, instanceContext, instanceClientTools, appContext, chatHost, agentDefinition, surfaceUserState, messages, userAuth, userProfile) => {
196
- const state = {
197
- conversations,
198
- instanceUIState,
199
- instanceUserInput,
200
- instanceResources,
201
- instanceVariableValues,
202
- instanceModelOverrides,
203
- instanceContext,
204
- instanceClientTools,
205
- appContext,
206
- chatHost,
207
- agentDefinition,
208
- surfaceUserState,
209
- messages,
210
- userAuth,
211
- userProfile
212
- };
213
- return assembleRequest(state, conversationId);
214
- }
215
- );
174
+ const inputs = [
175
+ (s) => s.conversations,
176
+ (s) => s.instanceUIState,
177
+ (s) => s.instanceUserInput,
178
+ (s) => s.instanceResources,
179
+ (s) => s.instanceVariableValues,
180
+ (s) => s.instanceModelOverrides,
181
+ (s) => s.instanceContext,
182
+ (s) => s.instanceClientTools,
183
+ (s) => selectAppContext(s),
184
+ (s) => s.chatHost,
185
+ // The context door (buildRequestContext) reads these too: the agent's
186
+ // policies + kill switch, the person's saved context rules, whether this
187
+ // is the first turn.
188
+ (s) => s.agentDefinition,
189
+ (s) => s.surfaceUserState,
190
+ (s) => s.messages
191
+ ];
192
+ let lastInputs = null;
193
+ let lastResult = null;
194
+ return (state) => {
195
+ const current = inputs.map((read) => read(state));
196
+ if (lastInputs && current.every((value, i) => value === lastInputs[i])) return lastResult;
197
+ lastInputs = current;
198
+ lastResult = assembleRequest(state, conversationId);
199
+ return lastResult;
200
+ };
216
201
  }
217
202
  const selectAssembledRequest = makeSelectAssembledRequest;
218
203
  const selectInstanceSummary = (conversationId) => createSelector(
@@ -0,0 +1,44 @@
1
+ /**
2
+ * THE CONVERSATION DOOR for every JSON request that names a conversation (Audit B family 8).
3
+ *
4
+ * A turn resolves where it goes and what it carries in `resolveBackendForConversation`: the
5
+ * conversation's channel (global / sandbox override / local engine / EC2-dedicated, under the
6
+ * confidential-routing rule), THE ONE header builder and THE ORG RULE. Until 2026-10-08 the calls
7
+ * that ride beside a turn on the same conversation — the inbox, tool results, pending calls,
8
+ * memory cost, cancel — went through the host's general `callApi` instead: another base URL (no
9
+ * sandbox / local override), another organization precedence (the tab's selection, not the
10
+ * conversation's), and in one host no session at all (the inbox sent without auth). This door
11
+ * makes them the turn's twin: same resolver, so the same server, credential and organization.
12
+ *
13
+ * Result shape and error classification are the shared pipeline's (`executeMatrxCall`,
14
+ * `normalizeMatrxError` in `@ai-matrx/agents/matrx`) — the same `{ data } | { error }` the call
15
+ * sites already read from `callApi`. Guard: `__tests__/conversation-calls-name-the-turns-org.test.ts`.
16
+ */
17
+ import { type MatrxCancelResponse, type MatrxCallResult, type MatrxQueryParams } from "@ai-matrx/agents/matrx";
18
+ import type { ChatRootState } from "../../../../store/root-state.js";
19
+ export type ConversationCallMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
20
+ export interface ConversationCallConfig {
21
+ /** Path template; `{conversation_id}` is filled from the door's conversation. */
22
+ path: string;
23
+ method: ConversationCallMethod;
24
+ /** Other path parameters (never `conversation_id`). */
25
+ pathParams?: Record<string, string>;
26
+ queryParams?: MatrxQueryParams;
27
+ body?: unknown;
28
+ signal?: AbortSignal;
29
+ /** HTTP statuses this call site handles as expected outcomes (not logged as failures). */
30
+ expectedErrorStatuses?: readonly number[];
31
+ connectTimeoutMs?: number;
32
+ totalTimeoutMs?: number | null;
33
+ }
34
+ type AnyDispatch = (action: never) => unknown;
35
+ /** Send one JSON request on `conversationId`'s own channel, credential and organization. */
36
+ export declare function callConversationApi<T = unknown>(conversationId: string, config: ConversationCallConfig): (dispatch: AnyDispatch, getState: () => ChatRootState) => Promise<MatrxCallResult<T>>;
37
+ /**
38
+ * Stop (or interrupt) one server run of `conversationId` on the conversation's own channel,
39
+ * credential and organization — never the tab's. `seenSeq` is the last wire `stream_seq` the page
40
+ * APPLIED before Stop; the server cuts the stopped answer there, so the saved text never outgrows
41
+ * the screen.
42
+ */
43
+ export declare function cancelConversationRun(conversationId: string, requestId: string, mode?: "cancel" | "interrupt", seenSeq?: number): (dispatch: AnyDispatch, getState: () => ChatRootState) => Promise<MatrxCallResult<MatrxCancelResponse>>;
44
+ export {};
@@ -0,0 +1,78 @@
1
+ import {
2
+ cancelAgentRun,
3
+ buildMatrxRequestBody,
4
+ buildMatrxRequestUrl,
5
+ executeMatrxCall,
6
+ normalizeMatrxError,
7
+ resolveEndpointPath
8
+ } from "@ai-matrx/agents/matrx";
9
+ import { selectEndpointOverrideConfig } from "../../../../host/server/api-config.js";
10
+ import { conversationRequestOrganizationId } from "../utils/required-organization.js";
11
+ import { resolveBackendForConversation } from "./resolve-base-url.js";
12
+ import { createMatrxTransportForConversation } from "./matrx-transport-for-conversation.js";
13
+ function callConversationApi(conversationId, config) {
14
+ return async (_dispatch, getState) => {
15
+ try {
16
+ const state = getState();
17
+ const backend = resolveBackendForConversation(state, conversationId);
18
+ if (!backend) {
19
+ return {
20
+ error: {
21
+ type: "network_error",
22
+ message: "No AI Matrx server is configured for this conversation. The request was not sent."
23
+ }
24
+ };
25
+ }
26
+ const organizationId = conversationRequestOrganizationId(state, conversationId);
27
+ const path = resolveEndpointPath(config.path, selectEndpointOverrideConfig(state));
28
+ const result = await executeMatrxCall({
29
+ url: buildMatrxRequestUrl(
30
+ backend.baseUrl,
31
+ path,
32
+ { ...config.pathParams ?? {}, conversation_id: conversationId },
33
+ config.queryParams
34
+ ),
35
+ method: config.method,
36
+ headers: { ...backend.headers, Accept: "application/json" },
37
+ body: buildMatrxRequestBody(
38
+ config.body,
39
+ organizationId ? { organization_id: organizationId } : {}
40
+ ),
41
+ ...config.signal ? { signal: config.signal } : {},
42
+ ...config.connectTimeoutMs !== void 0 ? { connectTimeoutMs: config.connectTimeoutMs } : {},
43
+ ...config.totalTimeoutMs !== void 0 ? { totalTimeoutMs: config.totalTimeoutMs } : {}
44
+ });
45
+ const status = result.error?.status;
46
+ if (result.error && !(status !== void 0 && config.expectedErrorStatuses?.includes(status))) {
47
+ console.warn(`[conversation-call] ${config.method} ${config.path} failed`, {
48
+ conversationId,
49
+ channel: backend.channel,
50
+ error: result.error
51
+ });
52
+ }
53
+ return result;
54
+ } catch (error) {
55
+ return { error: normalizeMatrxError(error) };
56
+ }
57
+ };
58
+ }
59
+ function cancelConversationRun(conversationId, requestId, mode = "cancel", seenSeq) {
60
+ return async (_dispatch, getState) => {
61
+ try {
62
+ const base = createMatrxTransportForConversation(getState, conversationId, {
63
+ source: "matrxTransport:cancel"
64
+ });
65
+ const transport = seenSeq === void 0 ? base : {
66
+ fetch: (path, init) => base.fetch(`${path}${path.includes("?") ? "&" : "?"}seen_seq=${seenSeq}`, init)
67
+ };
68
+ const data = await cancelAgentRun(transport, requestId, { mode });
69
+ return { data, requestId: data.request_id };
70
+ } catch (error) {
71
+ return { error: normalizeMatrxError(error) };
72
+ }
73
+ };
74
+ }
75
+ export {
76
+ callConversationApi,
77
+ cancelConversationRun
78
+ };
@@ -0,0 +1,41 @@
1
+ /**
2
+ * continueAgentJson — the NEXT turn of a conversation whose first turn ran headlessly
3
+ * (`runHeadlessAgentJson` with `keepInstance`), waited on exactly like the first.
4
+ *
5
+ * ONE CONVERSATION, MANY ROUNDS. A surface that builds something with an agent and then keeps
6
+ * talking about it (the Applet builder: build → automatic fix round → "Change it" → the person's
7
+ * replies) continues THAT conversation instead of starting a new one per round, so the person can
8
+ * read and answer it in one place and the agent sees every earlier round.
9
+ *
10
+ * A turn is either the person's words (`userInput` — only what a human typed, THE USER-INPUT LAW)
11
+ * or a turn the host writes (`hostTurn` — an activation line + which feature wrote it; never quoted
12
+ * as the person). Structured content for the turn rides `context` (named entries on the instance —
13
+ * a `null` value removes the key), never the message.
14
+ *
15
+ * It reuses THE machinery: `executeInstance` sends the continuation (`POST /ai/conversations/{id}`),
16
+ * `adoptHeadlessAgentJson` waits on it (polling, settle window, one result rule, the persistence
17
+ * seam). A conversation not in this tab yet (a refresh) is read first through `loadConversation`.
18
+ * Never throws: resolves a structured result, exactly like `runHeadlessAgentJson`.
19
+ */
20
+ import type { ChatDispatch, ChatRootState } from "../../../../store/root-state.js";
21
+ import type { RequestInitiation } from "../../../types/instance.types.js";
22
+ import { type HostTurnInput } from "./execute-instance.thunk.js";
23
+ import { type AdoptedAgentJsonOptions, type HeadlessAgentJsonResult } from "./run-headless-agent-json.js";
24
+ export interface ContinueAgentJsonOptions extends Pick<AdoptedAgentJsonOptions, "surfaceKey" | "timeoutMs" | "pollIntervalMs" | "settleMs" | "expect" | "onResult" | "signal" | "failureMessages"> {
25
+ conversationId: string;
26
+ /** What the person typed for this turn. Exclusive with `hostTurn`. */
27
+ userInput?: string;
28
+ /** A turn the host writes (the person said nothing). Exclusive with `userInput`. */
29
+ hostTurn?: HostTurnInput;
30
+ /** Per-turn named context: each key is written on the instance; `null` removes it. */
31
+ context?: Record<string, unknown>;
32
+ /** "user" when the person pressed send; "auto" for a host-started round. Default by turn kind. */
33
+ initiation?: RequestInitiation;
34
+ /** Fires the instant the turn's request exists (a live view binds to it). */
35
+ onRequestId?: (requestId: string) => void;
36
+ /** Keep the instance after the turn (the live view owns teardown). Default true. */
37
+ keepInstance?: boolean;
38
+ }
39
+ /** Which of the two turn kinds this is — exactly one, or a sentence why not. */
40
+ export declare function continuationTurnProblem(opts: Pick<ContinueAgentJsonOptions, "userInput" | "hostTurn">): string | null;
41
+ export declare function continueAgentJson(dispatch: ChatDispatch, getState: () => ChatRootState, opts: ContinueAgentJsonOptions): Promise<HeadlessAgentJsonResult>;
@@ -0,0 +1,66 @@
1
+ import { removeContextEntry, setContextEntries } from "../instance-context/instance-context.slice.js";
2
+ import { setUserInputText } from "../instance-user-input/instance-user-input.slice.js";
3
+ import { executeInstance } from "./execute-instance.thunk.js";
4
+ import { loadConversation } from "./load-conversation.thunk.js";
5
+ import {
6
+ adoptHeadlessAgentJson
7
+ } from "./run-headless-agent-json.js";
8
+ function continuationTurnProblem(opts) {
9
+ const typed = typeof opts.userInput === "string" && opts.userInput.trim().length > 0;
10
+ if (typed && opts.hostTurn) return "A turn is the person's words or a host turn, never both.";
11
+ if (!typed && !opts.hostTurn) return "Nothing to send: the turn has neither the person's words nor a host turn.";
12
+ if (opts.hostTurn && (!opts.hostTurn.text.trim() || !opts.hostTurn.reason.trim())) return "A host turn needs its text and its reason.";
13
+ return null;
14
+ }
15
+ async function continueAgentJson(dispatch, getState, opts) {
16
+ const { conversationId } = opts;
17
+ const problem = continuationTurnProblem(opts);
18
+ if (problem) return { success: false, data: null, fullResponse: "", error: problem, conversationId };
19
+ try {
20
+ if (!getState().conversations.byConversationId[conversationId]) {
21
+ await dispatch(loadConversation({ conversationId })).unwrap();
22
+ }
23
+ const context = Object.entries(opts.context ?? {});
24
+ const writes = context.filter(([, value]) => value !== null && value !== void 0);
25
+ for (const [key] of context.filter(([, value]) => value === null)) dispatch(removeContextEntry({ conversationId, key }));
26
+ if (writes.length) dispatch(setContextEntries({ conversationId, entries: writes.map(([key, value]) => ({ key, value })) }));
27
+ if (opts.userInput) dispatch(setUserInputText({ conversationId, text: opts.userInput }));
28
+ let requestId = null;
29
+ const sent = await dispatch(
30
+ executeInstance({
31
+ conversationId,
32
+ initiation: opts.initiation ?? (opts.hostTurn ? "auto" : "user"),
33
+ ...opts.hostTurn ? { hostTurn: opts.hostTurn } : {},
34
+ onRequestId: (id) => {
35
+ requestId = id;
36
+ opts.onRequestId?.(id);
37
+ }
38
+ })
39
+ );
40
+ if (executeInstance.rejected.match(sent)) {
41
+ const reason = typeof sent.payload === "string" ? sent.payload : sent.error.message ?? "The turn could not be sent.";
42
+ return { success: false, data: null, fullResponse: "", error: reason, conversationId, ...requestId ? { requestId } : {} };
43
+ }
44
+ return await adoptHeadlessAgentJson(dispatch, getState, {
45
+ requestId: sent.payload.requestId,
46
+ conversationId,
47
+ surfaceKey: opts.surfaceKey,
48
+ keepInstance: opts.keepInstance ?? true,
49
+ ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {},
50
+ ...opts.pollIntervalMs !== void 0 ? { pollIntervalMs: opts.pollIntervalMs } : {},
51
+ ...opts.settleMs !== void 0 ? { settleMs: opts.settleMs } : {},
52
+ ...opts.expect ? { expect: opts.expect } : {},
53
+ ...opts.onResult ? { onResult: opts.onResult } : {},
54
+ ...opts.signal ? { signal: opts.signal } : {},
55
+ ...opts.failureMessages ? { failureMessages: opts.failureMessages } : {}
56
+ });
57
+ } catch (err) {
58
+ const message = err instanceof Error ? err.message : String(err);
59
+ console.error("[continueAgentJson] the turn failed before it ran", { conversationId, err });
60
+ return { success: false, data: null, fullResponse: "", error: message, conversationId };
61
+ }
62
+ }
63
+ export {
64
+ continuationTurnProblem,
65
+ continueAgentJson
66
+ };
@@ -36,6 +36,8 @@ export declare function isEmptyUserInput(userInput: unknown): boolean;
36
36
  */
37
37
  export declare function refusesEmptyTurn(args: {
38
38
  retry: boolean;
39
+ /** A host-written turn is a message even with no `user_input`. */
40
+ hostTurn?: boolean;
39
41
  hasPriorTurns: boolean;
40
42
  cacheOnly: boolean;
41
43
  isEphemeral: boolean;
@@ -123,6 +125,24 @@ interface ExecuteInstanceArgs {
123
125
  * composer: the person may already be typing the next message in it.
124
126
  */
125
127
  submission?: FrozenSubmission;
128
+ /**
129
+ * A turn the HOST writes on a persisted conversation — never something the
130
+ * person typed (THE USER-INPUT LAW). Sent as `host_turn` instead of
131
+ * `user_input`: a short activation line the model reads, plus which host
132
+ * feature wrote it; the turn's content rides `context`. Only a continuation
133
+ * (the conversation already has turns). The Applet builder's automatic fix
134
+ * round is one (lane F6b).
135
+ */
136
+ hostTurn?: HostTurnInput;
137
+ }
138
+ /** `ConversationContinueRequest.host_turn` — what a host-written turn carries. */
139
+ export interface HostTurnInput {
140
+ /** The activation line the model reads. */
141
+ text: string;
142
+ /** Which host feature wrote it (≤80 chars). */
143
+ reason: string;
144
+ /** Extra stamps for the stored message. */
145
+ metadata?: Record<string, unknown>;
126
146
  }
127
147
  interface ExecuteInstanceResult {
128
148
  requestId: string;
@@ -155,7 +155,7 @@ function isEmptyUserInput(userInput) {
155
155
  });
156
156
  }
157
157
  function refusesEmptyTurn(args) {
158
- if (args.retry || !args.hasPriorTurns) return false;
158
+ if (args.retry || args.hostTurn || !args.hasPriorTurns) return false;
159
159
  if (!shouldContinuePersistedConversation(
160
160
  args.cacheOnly,
161
161
  args.hasPriorTurns,
@@ -254,7 +254,8 @@ const executeInstance = createAsyncThunk(
254
254
  initiation,
255
255
  onRequestId,
256
256
  surfaceRefreshed = false,
257
- submission
257
+ submission,
258
+ hostTurn
258
259
  }, { getState, dispatch, rejectWithValue: reject }) => {
259
260
  const requestId = generateRequestId();
260
261
  const rejectWithValue = (value, originalErrorName) => reject(
@@ -340,6 +341,7 @@ const executeInstance = createAsyncThunk(
340
341
  }
341
342
  if (refusesEmptyTurn({
342
343
  retry,
344
+ hostTurn: Boolean(hostTurn),
343
345
  hasPriorTurns: selectMessageCount(conversationId)(state) > 0,
344
346
  cacheOnly: instance.cacheOnly,
345
347
  isEphemeral: instance.isEphemeral === true,
@@ -535,11 +537,16 @@ const executeInstance = createAsyncThunk(
535
537
  ).replace("{conversation_id}", encodeURIComponent(conversationId));
536
538
  url = `${baseUrl}${convPath}`;
537
539
  routedPayload = buildContinuationBody(payload, {
540
+ hostTurn,
538
541
  retry: Boolean(retry),
539
542
  debug: Boolean(debug),
540
543
  cacheBypass: pendingBypass
541
544
  });
542
545
  } else {
546
+ if (hostTurn) {
547
+ console.error(`[execute-instance] a host turn ("${hostTurn.reason}") was sent on conversation "${conversationId}", which has no persisted turns \u2014 nothing was sent.`);
548
+ return rejectWithValue("A host turn continues a conversation; this one has no turns yet");
549
+ }
543
550
  const start = resolveStartPath({
544
551
  agentId: instance.agentId,
545
552
  pinnedVersionId: instance.initialAgentVersionId ?? null,
@@ -26,10 +26,7 @@ import { alwaysOnSurfaceKeys } from "../../../../surfaces/utils/always-on-contex
26
26
  import {
27
27
  fetchAgentRunTier
28
28
  } from "../../agent-definition/thunks.js";
29
- import {
30
- isSignedOutVisitor,
31
- NotAuthenticatedError
32
- } from "../../../../host/identity.js";
29
+ import { isSignedOutVisitor, NotAuthenticatedError, selectAccessToken } from "../../../../host/identity.js";
33
30
  import { applyLaunchModelOverrides } from "../instance-model-overrides/launch-model-overrides.js";
34
31
  import { selectAgentRunTier, selectAgentById } from "../../agent-definition/selectors.js";
35
32
  import { getShortcutRecordFromState } from "../../agent-shortcuts/selectors.js";
@@ -84,8 +81,9 @@ import {
84
81
  resolvePerLaunchMappings
85
82
  } from "./surface-scope-mapping.js";
86
83
  import { ensureAgentCatalog } from "../../../identity/agent-identity.js";
84
+ import { selectOrganizationId } from "../../../../host/org.js";
87
85
  function hasSignedInSession(state) {
88
- return Boolean(state.userAuth?.accessToken);
86
+ return Boolean(selectAccessToken(state));
89
87
  }
90
88
  const INTERACTIVE_MODES = /* @__PURE__ */ new Set([
91
89
  "modal-full",
@@ -144,7 +142,7 @@ const launchAgentExecution = createAsyncThunk("instances/launch", async (options
144
142
  );
145
143
  }
146
144
  if (!agentId) {
147
- const selectedOrganizationId = adminLaneOrganizationId() ?? getState().appContext?.organization_id ?? null;
145
+ const selectedOrganizationId = adminLaneOrganizationId() ?? selectOrganizationId(getState());
148
146
  const questionOrganizationId = organizationId ?? selectedOrganizationId ?? await (await import("../../../../host/org.js")).ensureOrgId(null);
149
147
  const displayKey = mandateKey;
150
148
  const resolved = await resolveMandate(displayKey, { organizationId: questionOrganizationId });
@@ -34,11 +34,7 @@ export interface NotifyAndPlayEntry<S> {
34
34
  run: (args: never, ctx: NotifyAndPlayContext<S>) => Promise<unknown>;
35
35
  };
36
36
  }
37
- export declare function createNotifyAndPlayDispatcher<S extends {
38
- userAuth?: {
39
- id?: string | null;
40
- } | null;
41
- }>(options: {
37
+ export declare function createNotifyAndPlayDispatcher<S>(options: {
42
38
  /** Redux action type prefix, e.g. "scribeTools/dispatch". */
43
39
  typePrefix: string;
44
40
  /** Human label used in the unknown-tool message, e.g. "scribe". */
@@ -1,12 +1,13 @@
1
1
  import { createAsyncThunk } from "@reduxjs/toolkit";
2
2
  import { extractErrorMessage } from "@ai-matrx/data/net";
3
3
  import { postDelegatedToolOutcome } from "./post-delegated-tool-outcome.js";
4
+ import { selectUserId } from "../../../../host/identity.js";
4
5
  function createNotifyAndPlayDispatcher(options) {
5
6
  const { typePrefix, family, isToolName, getEntry } = options;
6
7
  return createAsyncThunk(
7
8
  typePrefix,
8
9
  async ({ conversationId, requestId, callId, toolName, args }, { dispatch, getState }) => {
9
- const userId = getState().userAuth?.id ?? null;
10
+ const userId = selectUserId(getState());
10
11
  const base = { conversationId, requestId, callId, toolName };
11
12
  const fail = (errorType, message, durationMs) => postDelegatedToolOutcome(dispatch, {
12
13
  ...base,
@@ -1,5 +1,5 @@
1
- import { adminLaneHeadersFor, adminLaneOrganizationId } from "../../../../host/server/admin-lane.js";
2
- import { adminSeatReplyOrganizationId } from "../utils/required-organization.js";
1
+ import { chatRequestHeaders } from "../../../../host/request-headers.js";
2
+ import { conversationRequestOrganizationId } from "../utils/required-organization.js";
3
3
  import {
4
4
  selectResolvedBaseUrl,
5
5
  selectActiveServer
@@ -14,7 +14,6 @@ import {
14
14
  getCachedLocalEngine,
15
15
  supportsLocalAgentExecution
16
16
  } from "../../../../host/server/local-engine.js";
17
- import { selectOrganizationId } from "../../../../host/org.js";
18
17
  import { getChatHost, isChatHostConfigured } from "../../../../host/configure.js";
19
18
  import {
20
19
  ConfidentialRoutingRefusedError,
@@ -86,21 +85,12 @@ function resolveBackendUnchecked(state, conversationId) {
86
85
  const entry = state.instanceUIState?.byConversationId?.[conversationId] ?? null;
87
86
  const overrideUrl = entry?.serverOverrideUrl ?? null;
88
87
  const headers = {
89
- "Content-Type": "application/json"
88
+ "Content-Type": "application/json",
89
+ ...chatRequestHeaders(
90
+ { accessToken: selectAccessToken(state), fingerprintId: selectFingerprintId(state) },
91
+ conversationRequestOrganizationId(state, conversationId)
92
+ )
90
93
  };
91
- const accessToken = selectAccessToken(state);
92
- const fingerprintId = selectFingerprintId(state);
93
- if (accessToken) {
94
- headers["Authorization"] = `Bearer ${accessToken}`;
95
- } else if (fingerprintId) {
96
- headers["X-Fingerprint-ID"] = fingerprintId;
97
- }
98
- const conversationOrganizationId = state.conversations?.byConversationId?.[conversationId]?.organizationId ?? null;
99
- const organizationId = adminSeatReplyOrganizationId(state, conversationId) ?? conversationOrganizationId ?? adminLaneOrganizationId() ?? selectOrganizationId(state);
100
- if (organizationId) {
101
- headers["X-Organization-Id"] = organizationId;
102
- Object.assign(headers, adminLaneHeadersFor(organizationId));
103
- }
104
94
  if (overrideUrl) {
105
95
  const ref = resolveAgentSandboxRef(state, conversationId);
106
96
  if (ref && ref.kind !== "local-pc") {
@@ -1,5 +1,5 @@
1
1
  import { createAsyncThunk } from "@reduxjs/toolkit";
2
- import { cancelAgentRunRequest } from "../../../../host/server/matrx-transport.js";
2
+ import { cancelConversationRun } from "./call-conversation-api.js";
3
3
  import { fetchOperationsByLink } from "../../../runtime-reconnect/api.js";
4
4
  import { resolveBackendForConversation } from "./resolve-base-url.js";
5
5
  import { loadConversation } from "./load-conversation.thunk.js";
@@ -82,7 +82,7 @@ const settleAfterStop = createAsyncThunk(
82
82
  }
83
83
  if (op && !op.is_terminal && !cancelSentFor && op.request_id) {
84
84
  cancelSentFor = op.request_id;
85
- const result = await dispatch(cancelAgentRunRequest(op.request_id));
85
+ const result = await dispatch(cancelConversationRun(conversationId, op.request_id));
86
86
  if (result.error) {
87
87
  console.warn("[settle-after-stop] late server cancel failed", {
88
88
  conversationId,
@@ -37,7 +37,7 @@ import {
37
37
  selectInFlightRequestIds,
38
38
  settleAfterStop
39
39
  } from "./settle-after-stop.thunk.js";
40
- import { cancelAgentRunRequest } from "../../../../host/server/matrx-transport.js";
40
+ import { cancelConversationRun } from "./call-conversation-api.js";
41
41
  import { toast } from "../../../../host/notify.js";
42
42
  import { selectHostConversation } from "../host-conversations/host-conversations.selectors.js";
43
43
  import { sendToHost, stopHostTurn } from "../host-conversations/host-conversation.thunks.js";
@@ -508,7 +508,7 @@ const cancelExecution = createAsyncThunk(
508
508
  serverRequestId
509
509
  );
510
510
  void dispatch(
511
- cancelAgentRunRequest(serverRequestId, "cancel", seenSeq)
511
+ cancelConversationRun(conversationId, serverRequestId, "cancel", seenSeq)
512
512
  ).then((result) => {
513
513
  if (result.error) {
514
514
  console.warn(
@@ -564,7 +564,7 @@ const interruptAndSend = createAsyncThunk(
564
564
  serverRequestId
565
565
  );
566
566
  void dispatch(
567
- cancelAgentRunRequest(serverRequestId, "interrupt", seenSeq)
567
+ cancelConversationRun(conversationId, serverRequestId, "interrupt", seenSeq)
568
568
  ).then(
569
569
  (result) => {
570
570
  if (result.error) {
@@ -56,11 +56,17 @@ export declare const CONTINUATION_FIELD_ROUTING: {
56
56
  readonly max_iterations: "not assembled by this thunk";
57
57
  readonly max_retries_per_iteration: "not assembled by this thunk";
58
58
  readonly responder_agent_id: "coding-mirror replies only; not assembled by this thunk";
59
- readonly host_turn: "host-owned turns only; not assembled by this thunk";
59
+ readonly host_turn: "set by the builder from the hostTurn option (never copied from the payload)";
60
60
  };
61
61
  export interface ContinuationBodyOptions {
62
62
  retry: boolean;
63
+ /** A host-written turn: sent as `host_turn`, and `user_input` is omitted (the person said nothing). */
64
+ hostTurn?: {
65
+ text: string;
66
+ reason: string;
67
+ metadata?: Record<string, unknown>;
68
+ };
63
69
  debug: boolean;
64
70
  cacheBypass: unknown;
65
71
  }
66
- export declare function buildContinuationBody(payload: AssembledAgentStartRequest, { retry, debug, cacheBypass }: ContinuationBodyOptions): Record<string, unknown>;
72
+ export declare function buildContinuationBody(payload: AssembledAgentStartRequest, { retry, debug, cacheBypass, hostTurn }: ContinuationBodyOptions): Record<string, unknown>;
@@ -50,12 +50,12 @@ const CONTINUATION_FIELD_ROUTING = {
50
50
  max_iterations: "not assembled by this thunk",
51
51
  max_retries_per_iteration: "not assembled by this thunk",
52
52
  responder_agent_id: "coding-mirror replies only; not assembled by this thunk",
53
- host_turn: "host-owned turns only; not assembled by this thunk"
53
+ host_turn: "set by the builder from the hostTurn option (never copied from the payload)"
54
54
  };
55
55
  const EMPTY_IS_MEANINGFUL = /* @__PURE__ */ new Set(["tools", "output_kinds"]);
56
56
  const FALSE_IS_MEANINGFUL = /* @__PURE__ */ new Set(["memory", "tools_replace", "context_withheld"]);
57
- function buildContinuationBody(payload, { retry, debug, cacheBypass }) {
58
- const body = retry ? { retry: true } : { user_input: payload.user_input };
57
+ function buildContinuationBody(payload, { retry, debug, cacheBypass, hostTurn }) {
58
+ const body = retry ? { retry: true } : hostTurn ? { host_turn: hostTurn } : { user_input: payload.user_input };
59
59
  body.stream = true;
60
60
  const source = payload;
61
61
  for (const [key, route] of Object.entries(CONTINUATION_FIELD_ROUTING)) {
@@ -80,4 +80,22 @@ export declare function launchAsksForOrganizationBeforeOpen(launch: {
80
80
  opensWindow: boolean;
81
81
  }): boolean;
82
82
  export declare function ensureExecutionOrganization(state: ChatRootState, conversationId: string): Promise<string | null>;
83
+ /**
84
+ * THE ORG RULE (Audit B family 26): which organization a request on this conversation names.
85
+ * ONE function, read by the turn (`requireExecutionOrganizationId`) and by the conversation door
86
+ * every conversation-bound call rides (`resolveBackendForConversation`), so a turn and an inbox /
87
+ * tool-result / pending-calls / memory-cost / cancel call on the same conversation can never name
88
+ * different organizations. Guard: `__tests__/one-org-rule-per-conversation.test.ts`.
89
+ *
90
+ * - Not yet persisted (`cacheOnly !== false`, or not in the store): the organization a launcher
91
+ * declared, then the admin seat's platform tenant (only in a host with a database), then the
92
+ * app-selected organization.
93
+ * - Persisted: the row owns its organization forever. The one exception is a platform admin
94
+ * replying from the admin seat to a conversation they reach only as an admin: the request names
95
+ * the platform tenant (the admin lane admits nothing else); the row's own organization is
96
+ * restored server-side and never changes.
97
+ *
98
+ * Returns null when nothing names one (the caller decides whether that refuses).
99
+ */
100
+ export declare function conversationRequestOrganizationId(state: ChatRootState, conversationId: string): string | null;
83
101
  export declare function requireExecutionOrganizationId(state: ChatRootState, conversationId: string): string;