fix(runtime): align AppServer conversation scope

This commit is contained in:
2026-08-05 18:47:00 +08:00
parent d2a2fd0aff
commit 121ee6ac73
4 changed files with 38 additions and 18 deletions
@@ -664,7 +664,7 @@ export class LineUpRuntime {
v: 1,
id: this.newID("msg"),
type,
conversation_id: this.requireConversationID(),
conversation_id: this.wireConversationID(),
sender: { kind: "human", id: this.session.uid },
target: { kind: "agent", id: this.session.agent_uid },
timestamp: this.now(),
@@ -682,7 +682,7 @@ export class LineUpRuntime {
v: 1,
id: this.newID("msg"),
type,
conversation_id: this.requireConversationID(),
conversation_id: this.wireConversationID(),
sender: { kind: "human", id: this.session.uid },
target: { kind: "agent", id: this.session.agent_uid },
timestamp: this.now(),
@@ -745,6 +745,15 @@ export class LineUpRuntime {
private processTransportMessage(message: TransportMessage): void {
const payload = decodeTransportPayload(message.payload);
// AppServer sync is channel-wide, so a channel can contain messages from
// other users. They are not part of this Runtime's conversation and must
// never reach scope routing or the Chat projection.
if (message.from_uid !== this.session.uid
&& message.from_uid !== this.session.agent_uid
&& !message.from_uid.startsWith("agent_")) {
runtimeLog("message.ignored", { stage: "sender" });
return;
}
if (message.from_uid === this.session.uid) {
const own = parseEnvelopeJSON(payload);
if (own.ok && CONTROL_ECHO_TYPES.has(own.envelope.type)) return;
@@ -754,7 +763,7 @@ export class LineUpRuntime {
}
const scopeResolution = resolveIncomingScope(payload, {
app_scope: "chat", conversation_id: this.requireConversationID(),
app_scope: "chat", conversation_id: this.requireConversationID(), conversation_ids: [this.wireConversationID()],
acceptsAppScope: scope => Boolean(this.apps.get(scope)?.enabled),
});
if (scopeResolution.disposition === "rejected") {
@@ -762,7 +771,7 @@ export class LineUpRuntime {
this.emit({ type: "scope-rejected", reason: scopeResolution.reason, raw: payload });
return;
}
const route = this.toolRouter.route(payload, { app_scope: scopeResolution.scope.app_scope, conversation_id: this.requireConversationID(), inventory_revision: this.inventoryRevision });
const route = this.toolRouter.route(payload, { app_scope: scopeResolution.scope.app_scope, conversation_id: this.requireConversationID(), conversation_ids: [this.wireConversationID()], inventory_revision: this.inventoryRevision });
if (route.disposition === "rejected") {
runtimeLog("tool.rejected", { reason: route.code });
this.emit({ type: "scope-rejected", reason: route.code === "scope_mismatch" ? "scope_mismatch" : "invalid_scope", raw: payload });
@@ -1072,6 +1081,12 @@ export class LineUpRuntime {
if (!conversationID) throw new Error("LineUpRuntime has no active conversation.");
return conversationID;
}
/** AppServer's network conversation key; local persistence keeps Runtime's legacy key. */
private wireConversationID(): string {
if (!this.session.active) throw new Error("LineUpRuntime has no active conversation.");
return `lineup:${this.session.channel_type}:${this.session.channel_id}:${this.session.uid}`;
}
}
function boundedFailure(value: string): string {