@mulmobridge/chat-service 0.1.4 → 0.1.6

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.
@@ -11,7 +11,15 @@ export interface ChatStateStore {
11
11
  getChatState(transportId: string, externalChatId: string): Promise<TransportChatState | null>;
12
12
  setChatState(transportId: string, state: TransportChatState): Promise<void>;
13
13
  resetChatState(transportId: string, externalChatId: string, roleId: string): Promise<TransportChatState>;
14
- connectSession(transportId: string, externalChatId: string, chatSessionId: string): Promise<TransportChatState | null>;
14
+ /** Repoint the persisted chat state at another session. `roleId` is
15
+ * optional: when omitted, the existing state's roleId is preserved
16
+ * (the old default, kept for HTTP `/connect` callers that only know
17
+ * the session ID). Callers that DO know the target session's role —
18
+ * notably the `/switch` command, which already has `SessionSummary.roleId`
19
+ * from `/sessions` — MUST pass it, otherwise the file-backed state
20
+ * drifts into a stale-role / new-session pair and the next relay's
21
+ * `startChat` uses the mismatched pair (issue #1888). */
22
+ connectSession(transportId: string, externalChatId: string, chatSessionId: string, roleId?: string): Promise<TransportChatState | null>;
15
23
  generateSessionId(transportId: string, externalChatId: string): string;
16
24
  }
17
25
  export declare function createChatStateStore(opts: {
@@ -57,13 +57,18 @@ export function createChatStateStore(opts) {
57
57
  });
58
58
  return state;
59
59
  };
60
- const connectSession = async (transportId, externalChatId, chatSessionId) => {
60
+ const connectSession = async (transportId, externalChatId, chatSessionId, roleId) => {
61
61
  const existing = await getChatState(transportId, externalChatId);
62
62
  if (!existing)
63
63
  return null;
64
64
  const updated = {
65
65
  ...existing,
66
66
  sessionId: chatSessionId,
67
+ // A missing `roleId` arg means "preserve the current role" (that's
68
+ // the HTTP `/connect` route, which doesn't know the target's role);
69
+ // when the caller passes one (`/switch`), take theirs so state and
70
+ // downstream `startChat` agree on which role runs the resumed session.
71
+ ...(roleId !== undefined ? { roleId } : {}),
67
72
  updatedAt: new Date().toISOString(),
68
73
  };
69
74
  await setChatState(transportId, updated);
@@ -71,6 +76,7 @@ export function createChatStateStore(opts) {
71
76
  transportId,
72
77
  externalChatId,
73
78
  sessionId: chatSessionId,
79
+ roleId: updated.roleId,
74
80
  });
75
81
  return updated;
76
82
  };
package/dist/commands.js CHANGED
@@ -211,7 +211,12 @@ export function createCommandHandler(opts) {
211
211
  };
212
212
  }
213
213
  }
214
- const updated = await connectSession(transportId, chatState.externalChatId, target.id);
214
+ // Pass `target.roleId` so the persisted state's role tracks the session
215
+ // we just repointed at. Without this, `connectSession` would swap the
216
+ // sessionId but leave the previous role in place, and the next relay's
217
+ // `startChat` would run the resumed session under the wrong role
218
+ // (issue #1888).
219
+ const updated = await connectSession(transportId, chatState.externalChatId, target.id, target.roleId);
215
220
  if (!updated) {
216
221
  return { reply: "Failed to switch session." };
217
222
  }
package/dist/index.js CHANGED
@@ -97,7 +97,31 @@ export function createChatService(deps) {
97
97
  badRequest(res, "chatSessionId is required");
98
98
  return;
99
99
  }
100
- const updated = await store.connectSession(transportId, externalChatId, chatSessionId);
100
+ // Resolve the target session's role BEFORE calling connectSession so the
101
+ // persisted state's `roleId` tracks the new session's role — otherwise the
102
+ // next relay's `startChat` would resume the new session under the previous
103
+ // role (#1888 / #1894). Three fallback paths all treated as "preserve
104
+ // existing role":
105
+ // 1. No `getSessionRole` wired at all (backward compat for older hosts).
106
+ // 2. Resolver returns null (unknown / corrupt session metadata).
107
+ // 3. Resolver throws (host bug / timeout / IO error) — catch here so
108
+ // the route can never bubble the failure as a 500 to the API caller
109
+ // (codex review on #1895; the MulmoClaude host's resolver is
110
+ // hardened but the DI contract doesn't require hosts to be).
111
+ let resolvedRole = null;
112
+ if (deps.getSessionRole) {
113
+ try {
114
+ resolvedRole = await deps.getSessionRole(chatSessionId);
115
+ }
116
+ catch (err) {
117
+ logger.warn("chat-service", "getSessionRole threw; falling back to preserving existing role", {
118
+ chatSessionId,
119
+ error: err instanceof Error ? err.message : String(err),
120
+ });
121
+ resolvedRole = null;
122
+ }
123
+ }
124
+ const updated = await store.connectSession(transportId, externalChatId, chatSessionId, resolvedRole ?? undefined);
101
125
  if (!updated) {
102
126
  notFound(res, "No chat state found for this transport");
103
127
  return;
package/dist/types.d.ts CHANGED
@@ -122,6 +122,17 @@ export interface ChatServiceDeps {
122
122
  }>;
123
123
  total: number;
124
124
  }>;
125
+ /**
126
+ * Resolve the roleId a given session was started with. Used by the HTTP
127
+ * `/connect` route so the persisted bridge state's role tracks the target
128
+ * session's role after a repoint — same drift-fix as bridge `/switch`
129
+ * (issue #1888 / #1894), but for API callers that only supply a session ID.
130
+ * Returns null when the session isn't found OR when its role isn't known;
131
+ * on null the route falls back to the previous "preserve current role"
132
+ * behaviour (safe default). Omit this dep entirely to keep the old
133
+ * session-id-only semantics.
134
+ */
135
+ getSessionRole?: (sessionId: string) => Promise<string | null>;
125
136
  /**
126
137
  * Return the skills the bridge command handler should expose. The
127
138
  * handler uses the result for two things:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mulmobridge/chat-service",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Server-side chat service for MulmoBridge — socket.io + REST bridge to Claude Code agents",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",