Q147: the conversation header lists its real members, how each agent replies, and who can read
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
02309546dc
commit
983d37937d
13 files changed
+173
-44
No files matched your search
@@ -0,0 +1,18 @@
|
||||
import type { DatabaseSync } from "node:sqlite";
|
||||
|
||||
/** The agent that answers a channel message naming no one: its most senior active member with an enabled provider. */
|
||||
export function channelAnchorAgentId(db: DatabaseSync, orgId: string, spaceId: string): string | null {
|
||||
const anchor = db.prepare(`
|
||||
select a.id as agent_id
|
||||
from chat_memberships cm
|
||||
join agents a on a.id = cm.principal_id and a.org_id = ?
|
||||
join providers p on p.id = a.model_provider_id and p.org_id = a.org_id
|
||||
where cm.space_id = ? and cm.principal_type = 'agent'
|
||||
and a.lifecycle = 'active'
|
||||
and p.enabled = 1
|
||||
order by case a.role_level when 'c_suite' then 0 when 'director' then 1 when 'lead' then 2 else 3 end,
|
||||
a.created_at asc
|
||||
limit 1
|
||||
`).get(orgId, spaceId) as { agent_id: string } | undefined;
|
||||
return anchor?.agent_id ?? null;
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { channelAnchorAgentId } from "./chat-anchor";
|
||||
import { readVerifiedFile } from "./files/file-object-store";
|
||||
import type { DatabaseSync } from "node:sqlite";
|
||||
import type { CreateChatSpaceInput, DeleteMessageInput, EditMessageInput, MarkChatSpaceReadInput, SendMessageInput } from "../../shared/commands";
|
||||
@@ -313,24 +314,13 @@ export class ChatService {
|
||||
* exactly like a room going quiet until the highest-ranking person speaks.
|
||||
*/
|
||||
private wakeChannelAnchor(input: SendMessageInput, space: SpaceRow, messageId: string, threadId: string, now: string): void {
|
||||
const anchor = this.db.prepare(`
|
||||
select a.id as agent_id
|
||||
from chat_memberships cm
|
||||
join agents a on a.id = cm.principal_id and a.org_id = ?
|
||||
join providers p on p.id = a.model_provider_id and p.org_id = a.org_id
|
||||
where cm.space_id = ? and cm.principal_type = 'agent'
|
||||
and a.lifecycle = 'active'
|
||||
and p.enabled = 1
|
||||
order by case a.role_level when 'c_suite' then 0 when 'director' then 1 when 'lead' then 2 else 3 end,
|
||||
a.created_at asc
|
||||
limit 1
|
||||
`).get(input.orgId, input.spaceId) as { agent_id: string } | undefined;
|
||||
if (!anchor) {
|
||||
const anchorId = channelAnchorAgentId(this.db, input.orgId, input.spaceId);
|
||||
if (!anchorId) {
|
||||
console.info(`[chat] no runnable agent member in #${space.name}; the message stays unanswered until one is hired or a provider is enabled.`);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.agentWake.wakeFromMention(input, anchor.agent_id, messageId, threadId, now);
|
||||
this.agentWake.wakeFromMention(input, anchorId, messageId, threadId, now);
|
||||
} catch (error) {
|
||||
console.warn(`[chat] waking channel anchor for #${space.name} failed: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { currentProviderUsageWindow } from "../../shared/provider-runtime-domain
|
||||
import { buildUsageStats } from "./usage-stats";
|
||||
import { taskHolds } from "./task-holds";
|
||||
import { chatReplyStatuses } from "./chat-reply-status";
|
||||
import { channelAnchorAgentId } from "./chat-anchor";
|
||||
|
||||
type Row = Record<string, unknown>;
|
||||
type ProviderUsageWindowRow = Pick<
|
||||
@@ -100,6 +101,11 @@ export class SnapshotService {
|
||||
chatMemberships: list(this.db, "chat_memberships", "space_id asc, principal_type asc, principal_id asc"),
|
||||
chatMessages,
|
||||
chatReplyStatuses: chatReplyStatuses(this.db, chatMessages.map((message) => message.id), holds),
|
||||
chatSpaceAnchors: (this.db.prepare("select id, org_id from chat_spaces where space_type <> 'direct'").all() as Array<{ id: string; org_id: string }>)
|
||||
.flatMap((space) => {
|
||||
const agentId = channelAnchorAgentId(this.db, space.org_id, space.id);
|
||||
return agentId ? [{ spaceId: space.id, agentId }] : [];
|
||||
}),
|
||||
// Every thread a loaded message belongs to, plus the most recently active ones, so no shown message loses its thread.
|
||||
chatThreads: all<Row>(this.db.prepare(`
|
||||
select * from chat_threads
|
||||
|
||||
Reference in new issue
Block a user