From bda70c782861a38ca274c6b7653c6f6707ca7912 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Thu, 29 Oct 2026 20:38:49 -0700 Subject: [PATCH 1/4] feat(pod): say which agents aren't running, and name the one Scout's reply didn't reach Two silences from the census of people who heard nothing in their first pod. A group pod now shows a status row for each installed agent that never connected ("X isn't connected yet") or went quiet ("X last checked in 3h ago"), from the reach states agent-states already measures. Agent rooms keep their own row; hosted, listening and unknown agents show nothing. The "No agent was notified" hint stayed hidden whenever a wake-on-message agent woke. In a pod with Scout and the person's own hosted agent, Scout answered and the hosted agent never heard the message. Delivery now returns wokenAgents, and when an active agent wasn't woken the hint names it: "Scout will answer this. Other agents here answer only when you @mention them. Try @sly." Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01N4VWLC9MiWprFWXFNjJSnr --- backend/__tests__/service/pgMessages.test.js | 2 + .../controllers/messageController.test.js | 6 +- .../controllers/pgMessageController.test.js | 3 +- .../messageAgentDeliveryService.test.js | 3 +- .../services/messageAgentDeliveryService.ts | 3 + frontend/src/i18n/locales/en.json | 5 + frontend/src/i18n/locales/zh-CN.json | 5 + .../v2/__tests__/V2PodAgentLiveness.test.tsx | 124 ++++++++++++++++++ .../__tests__/V2ThreadDeliveryHint.test.tsx | 42 ++++++ frontend/src/v2/components/V2Thread.tsx | 55 +++++++- .../src/v2/components/V2ThreadMessages.tsx | 6 +- frontend/src/v2/hooks/useV2PodDetail.ts | 2 + 12 files changed, 245 insertions(+), 11 deletions(-) create mode 100644 frontend/src/v2/__tests__/V2PodAgentLiveness.test.tsx diff --git a/backend/__tests__/service/pgMessages.test.js b/backend/__tests__/service/pgMessages.test.js index 00228d5c82..0b107eaaed 100644 --- a/backend/__tests__/service/pgMessages.test.js +++ b/backend/__tests__/service/pgMessages.test.js @@ -136,6 +136,7 @@ describe('PostgreSQL Message Routes', () => { implicit: [], agentsInPod: expect.any(Number), woken: 0, + wokenAgents: [], }); }); @@ -188,6 +189,7 @@ describe('PostgreSQL Message Routes', () => { implicit: [], agentsInPod: expect.any(Number), woken: 0, + wokenAgents: [], }, }); expect(AgentMentionService.enqueueMentions).toHaveBeenCalledWith({ diff --git a/backend/__tests__/unit/controllers/messageController.test.js b/backend/__tests__/unit/controllers/messageController.test.js index 122b11c8c3..962744dc95 100644 --- a/backend/__tests__/unit/controllers/messageController.test.js +++ b/backend/__tests__/unit/controllers/messageController.test.js @@ -254,7 +254,7 @@ describe('messageController', () => { expect(res.json).toHaveBeenCalledWith({ ...mockMessage, agentDelivery: { - enqueued: 0, implicit: [], agentsInPod: 2, woken: 0, + enqueued: 0, implicit: [], agentsInPod: 2, woken: 0, wokenAgents: [], }, }); expect(AgentMentionService.enqueueMentions).toHaveBeenCalled(); @@ -287,7 +287,7 @@ describe('messageController', () => { expect(res.json).toHaveBeenCalledWith({ ...mockMessage, agentDelivery: { - enqueued: 1, implicit: [], agentsInPod: 2, woken: 0, + enqueued: 1, implicit: [], agentsInPod: 2, woken: 0, wokenAgents: [], }, }); }); @@ -502,7 +502,7 @@ describe('messageController', () => { expect(res.json).toHaveBeenCalledWith({ ...mockMessage, agentDelivery: { - enqueued: 0, implicit: [], agentsInPod: 1, woken: 1, + enqueued: 0, implicit: [], agentsInPod: 1, woken: 1, wokenAgents: ['guide'], }, }); }); diff --git a/backend/__tests__/unit/controllers/pgMessageController.test.js b/backend/__tests__/unit/controllers/pgMessageController.test.js index 45b474306f..df3d350234 100644 --- a/backend/__tests__/unit/controllers/pgMessageController.test.js +++ b/backend/__tests__/unit/controllers/pgMessageController.test.js @@ -80,7 +80,7 @@ describe('pgMessageController', () => { AgentMentionService.enqueueMentions.mockResolvedValue({ enqueued: [{ installationId: 'i1' }], implicit: ['recorder'], - woken: [{ installationId: 'i2' }], + woken: ['guide'], }); AgentInstallation.countDocuments.mockResolvedValue(2); const req = { @@ -103,6 +103,7 @@ describe('pgMessageController', () => { implicit: ['recorder'], agentsInPod: 2, woken: 1, + wokenAgents: ['guide'], }, }); }); diff --git a/backend/__tests__/unit/services/messageAgentDeliveryService.test.js b/backend/__tests__/unit/services/messageAgentDeliveryService.test.js index 639b4beeeb..298b4ced88 100644 --- a/backend/__tests__/unit/services/messageAgentDeliveryService.test.js +++ b/backend/__tests__/unit/services/messageAgentDeliveryService.test.js @@ -26,7 +26,7 @@ describe('messageAgentDeliveryService', () => { AgentMentionService.enqueueMentions.mockResolvedValue({ enqueued: [{ installationId: 'mention-1' }], implicit: ['recorder'], - woken: [{ installationId: 'wake-1' }], + woken: ['guide'], }); AgentInstallation.countDocuments.mockResolvedValue(2); }); @@ -66,6 +66,7 @@ describe('messageAgentDeliveryService', () => { implicit: ['recorder'], agentsInPod: 2, woken: 1, + wokenAgents: ['guide'], }, }); }); diff --git a/backend/services/messageAgentDeliveryService.ts b/backend/services/messageAgentDeliveryService.ts index dcab8ccde8..4c225a4566 100644 --- a/backend/services/messageAgentDeliveryService.ts +++ b/backend/services/messageAgentDeliveryService.ts @@ -35,6 +35,8 @@ interface AgentDelivery { implicit: string[]; agentsInPod: number; woken: number; + // Who was woken, so the client can name an agent that was not. + wokenAgents: string[]; } type DeliveredMessage = T | (T & { mentionNotices?: unknown[]; agentDelivery?: AgentDelivery }); @@ -116,6 +118,7 @@ export const deliverMessageToAgents = async ( implicit: mentionResult.implicit || [], agentsInPod, woken: mentionResult.woken?.length ?? 0, + wokenAgents: mentionResult.woken || [], }, }; }; diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 3dbd8b52a5..5efb985eb7 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -566,6 +566,7 @@ "cancelReply": "Cancel reply", "replyingTo": "Replying to {{author}}:", "deliveryHint": "No agent was notified — @mention one to get a reply. Try @{{handle}}.", + "deliveryHintOthers": "{{woken}} will answer this. Other agents here answer only when you @mention them. Try @{{handle}}.", "mentionNotInPod": "{{handle}} isn't in this pod. Invite them, or ask in a pod they're in.", "mentionNotInPodWithPods": "{{handle}} isn't in this pod. Ask in:", "noAgentsInPod": { @@ -580,6 +581,10 @@ "waiting": "Waiting for {{agentName}} to reply…", "timedOut": "{{agentName}} hasn't replied yet. Your message is still in this conversation." }, + "podAgentLiveness": { + "notConnected": "{{agentName}} isn't connected yet, so it won't answer here until it's running.", + "offlineSince": "{{agentName}} last checked in {{age}}. It answers when it's back." + }, "typing": { "one": "{{name}} is thinking…", "two": "{{first}} and {{second}} are thinking…", diff --git a/frontend/src/i18n/locales/zh-CN.json b/frontend/src/i18n/locales/zh-CN.json index 4755445bb2..4449cd6676 100644 --- a/frontend/src/i18n/locales/zh-CN.json +++ b/frontend/src/i18n/locales/zh-CN.json @@ -566,6 +566,7 @@ "cancelReply": "取消回复", "replyingTo": "回复 {{author}}:", "deliveryHint": "尚未通知任何智能体。@提及一个智能体即可让它回复,例如 @{{handle}}。", + "deliveryHintOthers": "{{woken}} 会回复这条消息。这里的其他智能体只在你 @提及 时回复,试试 @{{handle}}。", "mentionNotInPod": "{{handle}} 不在这个 Pod 中。请邀请对方加入,或到对方已加入的 Pod 中提问。", "mentionNotInPodWithPods": "{{handle}} 不在这个 Pod 中。可前往这些 Pod 提问:", "noAgentsInPod": { @@ -580,6 +581,10 @@ "waiting": "正在等待 {{agentName}} 回复…", "timedOut": "{{agentName}} 还没有回复。你的消息仍在此对话中。" }, + "podAgentLiveness": { + "notConnected": "{{agentName}} 还没有连接,运行起来之前不会在这里回复。", + "offlineSince": "{{agentName}} 上次接入是 {{age}},恢复后会回复。" + }, "typing": { "one": "{{name}} 正在思考…", "two": "{{first}} 和 {{second}} 正在思考…", diff --git a/frontend/src/v2/__tests__/V2PodAgentLiveness.test.tsx b/frontend/src/v2/__tests__/V2PodAgentLiveness.test.tsx new file mode 100644 index 0000000000..7bfddeb8d9 --- /dev/null +++ b/frontend/src/v2/__tests__/V2PodAgentLiveness.test.tsx @@ -0,0 +1,124 @@ +// @ts-nocheck +// A group pod names each installed agent that isn't running, so a newcomer +// doesn't wait on a reply that can't come. Silence census 10-30: 5 of 7 people +// who heard nothing in their first pod had an agent that never connected. +import React from 'react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; +import V2Thread from '../components/V2Thread'; +import { AuthContext } from '../../context/AuthContext'; + +jest.mock('../../context/SocketContext', () => ({ + useSocket: () => ({ socket: null, connected: false }), +})); + +beforeAll(() => { + Element.prototype.scrollIntoView = jest.fn(); +}); + +jest.mock('axios', () => { + const mock = { + get: jest.fn(() => Promise.resolve({ data: {} })), + post: jest.fn(() => Promise.resolve({ data: {} })), + patch: jest.fn(), + delete: jest.fn(), + defaults: { baseURL: '', headers: { common: {} } }, + interceptors: { + request: { use: jest.fn(), eject: jest.fn() }, + response: { use: jest.fn(), eject: jest.fn() }, + }, + }; + return { __esModule: true, default: mock, ...mock }; +}); + +const axios = jest.requireMock('axios').default; + +const authValue = { + currentUser: { _id: 'u1', username: 'solo-user' }, + user: { _id: 'u1', username: 'solo-user' }, + token: 't', + loading: false, + error: null, + isAuthenticated: true, + register: jest.fn(), + login: jest.fn(), + logout: jest.fn(), + updateProfile: jest.fn(), +}; + +const makeDetail = (overrides = {}) => ({ + pod: { _id: 'p1', name: 'My Workspace', type: 'chat' }, + members: [{ _id: 'u1', username: 'solo-user', isBot: false }], + messages: [], + agents: [{ agentName: 'sam-agent', instanceId: 'default', displayName: 'Sam Agent' }], + sendMessage: jest.fn(() => Promise.resolve({ _id: 'm1' })), + loading: false, + error: null, + refresh: jest.fn(), + ...overrides, +}); + +const mockStates = (agents) => { + axios.get.mockImplementation((url) => { + if (typeof url === 'string' && url.endsWith('/agent-states')) { + return Promise.resolve({ data: { agents } }); + } + return Promise.resolve({ data: {} }); + }); +}; + +const renderChat = (detail) => render( + + + + + , +); + +afterEach(() => jest.clearAllMocks()); + +const HOURS_AGO_3 = new Date(Date.now() - 3 * 60 * 60 * 1000).toISOString(); + +describe('group pod agent liveness', () => { + test('names an agent that never connected', async () => { + mockStates([{ agentName: 'sam-agent', instanceId: 'default', displayName: 'Sam Agent', state: 'never-connected', lastUsedAt: null, isOwner: true }]); + renderChat(makeDetail()); + + const row = await screen.findByTestId('pod-agent-liveness'); + expect(row).toHaveTextContent("Sam Agent isn't connected yet"); + }); + + test('says when an agent that went quiet last checked in', async () => { + mockStates([{ agentName: 'sam-agent', instanceId: 'default', displayName: 'Sam Agent', state: 'gone-dark', lastUsedAt: HOURS_AGO_3, isOwner: false }]); + renderChat(makeDetail()); + + const row = await screen.findByTestId('pod-agent-liveness'); + expect(row).toHaveTextContent('Sam Agent last checked in 3h ago'); + }); + + test.each([ + ['listening', { state: 'listening', lastUsedAt: HOURS_AGO_3 }], + ['hosted (reachable)', { state: 'reachable', lastUsedAt: null, hosted: true }], + ['unknown', { state: 'unknown', lastUsedAt: null }], + ])('shows nothing for an agent that is %s', async (_label, fields) => { + mockStates([{ agentName: 'sam-agent', instanceId: 'default', displayName: 'Sam Agent', isOwner: true, ...fields }]); + renderChat(makeDetail()); + + await waitFor(() => expect(axios.get).toHaveBeenCalledWith(expect.stringContaining('/agent-states'), expect.anything())); + expect(screen.queryByTestId('pod-agent-liveness')).not.toBeInTheDocument(); + }); + + test('a 1:1 agent room keeps its own row and never doubles it', async () => { + mockStates([{ agentName: 'sam-agent', instanceId: 'default', displayName: 'Sam Agent', state: 'never-connected', lastUsedAt: null, isOwner: true }]); + renderChat(makeDetail({ + pod: { _id: 'p1', name: 'Sam Agent', type: 'agent-room' }, + members: [ + { _id: 'u1', username: 'solo-user', isBot: false }, + { _id: 'b1', username: 'sam-agent', isBot: true }, + ], + })); + + await screen.findByTestId('agent-room-liveness'); + expect(screen.queryByTestId('pod-agent-liveness')).not.toBeInTheDocument(); + }); +}); diff --git a/frontend/src/v2/__tests__/V2ThreadDeliveryHint.test.tsx b/frontend/src/v2/__tests__/V2ThreadDeliveryHint.test.tsx index 8027b5106c..fd27a12a26 100644 --- a/frontend/src/v2/__tests__/V2ThreadDeliveryHint.test.tsx +++ b/frontend/src/v2/__tests__/V2ThreadDeliveryHint.test.tsx @@ -418,4 +418,46 @@ describe('V2Thread agent delivery hint', () => { expect(screen.queryByText('no agent in this pod')).not.toBeInTheDocument(); }); }); + + // Silence census, 10-20: Scout woke on an unmentioned message, the person's + // own hosted agent did not, and the hint stayed hidden because "an agent + // was woken". Name the agent that wasn't. + const SCOUT_AND_OWN = [ + { agentName: 'guide', instanceId: 'u3f9c2a1b7d', displayName: 'Scout', status: 'active' }, + { agentName: 'sly-agent', instanceId: 'default', displayName: 'Sly', status: 'active' }, + ]; + + test('when Scout woke and another agent did not, names that agent', async () => { + renderHarness( + makeMessage('can you research this', { + enqueued: 0, implicit: [], agentsInPod: 2, woken: 1, wokenAgents: ['guide'], + }), + jest.fn(), + { agents: SCOUT_AND_OWN }, + ); + + sendDraft('can you research this'); + + const hint = await screen.findByText(/will answer this/); + expect(hint.closest('.v2-chat__delivery-hint')).toHaveTextContent( + 'Scout will answer this. Other agents here answer only when you @mention them. Try @sly.', + ); + }); + + test('stays hidden when every agent in the pod was woken', async () => { + const sendSpy = jest.fn(); + renderHarness( + makeMessage('hello all', { + enqueued: 0, implicit: [], agentsInPod: 2, woken: 2, wokenAgents: ['guide', 'sly-agent'], + }), + sendSpy, + { agents: SCOUT_AND_OWN }, + ); + + sendDraft('hello all'); + await screen.findByText('hello all'); + + expect(screen.queryByText(/will answer this/)).not.toBeInTheDocument(); + expect(screen.queryByText(/No agent was notified/i)).not.toBeInTheDocument(); + }); }); diff --git a/frontend/src/v2/components/V2Thread.tsx b/frontend/src/v2/components/V2Thread.tsx index ac2ac98b35..3939591b92 100644 --- a/frontend/src/v2/components/V2Thread.tsx +++ b/frontend/src/v2/components/V2Thread.tsx @@ -34,6 +34,7 @@ import { useWelcomeHostedStart, WELCOME_HOSTED_ACTION } from '../hooks/useWelcom import { buildThreadView, freezeOrphanReplyIds } from '../utils/threadView'; import { agentKeyFor } from '../utils/agentKey'; import { runtimeChipLabel } from '../utils/runtimeChip'; +import { localizeRelativeTime } from '../utils/localizeRelativeTime'; import { buildAgentUsername, isOpaqueInstanceToken, @@ -79,11 +80,14 @@ interface AgentStateRow { displayName?: string; state: AgentReachState; isOwner: boolean; + /** Last time the agent's own runtime called in; null when it never has. */ + lastUsedAt?: string | null; /** A Commonly-hosted agent; absent from an older backend. */ hosted?: boolean; fixCommand?: string; } const REACH_IS_ACTIVE = new Set(['listening', 'reachable']); +const POD_LIVENESS_MAX_ROWS = 3; interface DirectAgentIdentity { username: string; @@ -266,6 +270,8 @@ const V2Thread: React.FC = ({ detail, firstRunVisible = false, in const [agentDeliveryHint, setAgentDeliveryHint] = useState<{ messageId: string; mentionHandle: string; + // The wake-on-message agent that did answer, when another agent did not. + wokenAgent?: string; } | null>(null); // A send into a pod with no installed agent: the delivery hint below is // deliberately gated on `agentsInPod > 0`, which is exactly the case this is @@ -1527,6 +1533,14 @@ const V2Thread: React.FC = ({ detail, firstRunVisible = false, in : directAgentReach === 'gone-dark' ? 'podChat.agentRoomLiveness.goneDark' : 'podChat.agentRoomLiveness.unknown'; + // Group pods name each agent that isn't running, so nobody waits on a reply + // that can't come: 5 of 7 people who heard nothing in their first pod had an + // agent that never connected (silence census, 10-30). Only reach states the + // server measured; an unknown state shows nothing. + const podAgentLiveness = isAgentRoom || isAgentDm ? [] : agentStates.filter((s) => ( + typeof s?.agentName === 'string' && s.agentName + && (s.state === 'never-connected' || s.state === 'gone-dark') + )).slice(0, POD_LIVENESS_MAX_ROWS); const showAwaitingAgentReply = Boolean( isAgentRoom && awaitingAgentReply && awaitingAgentReply.podId === pod._id, ); @@ -1584,7 +1598,20 @@ const V2Thread: React.FC = ({ detail, firstRunVisible = false, in }); } const delivery = created.agentDelivery; - const exampleAgent = agents.find((agent) => agent.status === 'active') || agents[0]; + // A wake-on-message agent (Scout) answering does not mean the person + // reached the agent they came for: in a pod with Scout and their own + // hosted agent, an unmentioned message woke Scout alone and the hint + // stayed hidden (silence census, 10-20). Name an agent that wasn't woken. + const wokenNames = new Set((delivery?.wokenAgents || []).map((name) => String(name).toLowerCase())); + const isWoken = (agent: (typeof agents)[number]) => wokenNames.has(String(agent?.agentName || '').toLowerCase()); + const anyWoken = (delivery?.woken ?? 0) > 0; + const unwokenAgent = anyWoken && wokenNames.size > 0 + ? agents.find((agent) => agent.status === 'active' && !isWoken(agent)) + : undefined; + const wokenAgent = unwokenAgent ? agents.find(isWoken) : undefined; + const exampleAgent = anyWoken + ? unwokenAgent + : (agents.find((agent) => agent.status === 'active') || agents[0]); // Same handle rule as the typeahead: human-chosen instanceId is the // identity and stays; an opaque per-user token must never be the // suggested handle ("Try @u3f9c2a1b7d") — persona displayName slug @@ -1602,7 +1629,8 @@ const V2Thread: React.FC = ({ detail, firstRunVisible = false, in // A wake-on-message agent (the Guide in every fresh workspace) WAS // notified — "No agent was notified" would be false on screen while // it's already typing (#914). - && (delivery.woken ?? 0) === 0 + // Unless another agent wasn't woken: then name that one. + && (!anyWoken || unwokenAgent) // A plain reply to an agent's message reaches it through the // reply-edge (implicit) path, not a mention. That agent WILL answer, // so "No agent was notified" would be false on screen. @@ -1624,7 +1652,13 @@ const V2Thread: React.FC = ({ detail, firstRunVisible = false, in } catch { // sessionStorage unavailable; the in-memory guard still works. } - setAgentDeliveryHint({ messageId: created.id, mentionHandle }); + setAgentDeliveryHint({ + messageId: created.id, + mentionHandle, + ...(wokenAgent ? { + wokenAgent: wokenAgent.displayName || wokenAgent.profile?.displayName || wokenAgent.agentName, + } : {}), + }); } } else if ( delivery @@ -2007,6 +2041,21 @@ const V2Thread: React.FC = ({ detail, firstRunVisible = false, in {t(directAgentLivenessKey, { agentName: directAgent.displayName })} )} + {podAgentLiveness.map((row) => ( +
+ {row.state === 'never-connected' || !row.lastUsedAt + ? t('podChat.podAgentLiveness.notConnected', { agentName: row.displayName || row.agentName }) + : t('podChat.podAgentLiveness.offlineSince', { + agentName: row.displayName || row.agentName, + age: localizeRelativeTime(row.lastUsedAt, t), + })} +
+ ))} {showAwaitingAgentReply && awaitingAgentReply && (
= ({ {agentDeliveryHint?.messageId === message.id && (
}} />
diff --git a/frontend/src/v2/hooks/useV2PodDetail.ts b/frontend/src/v2/hooks/useV2PodDetail.ts index 88182ea11a..bb2893fcea 100644 --- a/frontend/src/v2/hooks/useV2PodDetail.ts +++ b/frontend/src/v2/hooks/useV2PodDetail.ts @@ -60,6 +60,8 @@ export interface V2Message { agentsInPod: number; // Wake-on-message targets (#914). Optional: older backends omit it. woken?: number; + // Who was woken; older backends omit it. + wokenAgents?: string[]; }; // Sender-only feedback from the POST response. Socket broadcasts and // history omit it, so other pod members never see the notice. From 525540453234008a57ad38caa218fc7c09e0ad5b Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Thu, 29 Oct 2026 20:49:25 -0700 Subject: [PATCH 2/4] test(i18n): declare the woken agent name as an opaque placeholder Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01N4VWLC9MiWprFWXFNjJSnr --- frontend/src/i18n/__tests__/zhNumberUnitSpacing.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/src/i18n/__tests__/zhNumberUnitSpacing.test.ts b/frontend/src/i18n/__tests__/zhNumberUnitSpacing.test.ts index f4b568d7e1..a4e9ef7157 100644 --- a/frontend/src/i18n/__tests__/zhNumberUnitSpacing.test.ts +++ b/frontend/src/i18n/__tests__/zhNumberUnitSpacing.test.ts @@ -50,6 +50,8 @@ const OPAQUE_PLACEHOLDERS = [ 'requested', 'served', // runTrace.call inserts the opaque PG call ID, not a number. 'id', + // podChat.deliveryHintOthers names the agent that was woken. + 'woken', ]; // The exception runs the safe way: a name that is usually opaque and is a number From 4466792c44bc6a5fef4b2c6243d31df500a3d081 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Thu, 29 Oct 2026 21:49:19 -0700 Subject: [PATCH 3/4] =?UTF-8?q?fix(i18n):=20use=20=E8=BF=9E=E6=8E=A5=20in?= =?UTF-8?q?=20both=20pod=20liveness=20strings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01N4VWLC9MiWprFWXFNjJSnr --- frontend/src/i18n/locales/zh-CN.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/i18n/locales/zh-CN.json b/frontend/src/i18n/locales/zh-CN.json index 4449cd6676..847591f93b 100644 --- a/frontend/src/i18n/locales/zh-CN.json +++ b/frontend/src/i18n/locales/zh-CN.json @@ -583,7 +583,7 @@ }, "podAgentLiveness": { "notConnected": "{{agentName}} 还没有连接,运行起来之前不会在这里回复。", - "offlineSince": "{{agentName}} 上次接入是 {{age}},恢复后会回复。" + "offlineSince": "{{agentName}} 上次连接是 {{age}},恢复连接后会回复。" }, "typing": { "one": "{{name}} 正在思考…", From 8190a6d7b68170cf1e0a3c36591249518b377db5 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Thu, 29 Oct 2026 23:16:02 -0700 Subject: [PATCH 4/4] docs(models): the 168h age-out is AgentEvent's, not the episode's Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01N4VWLC9MiWprFWXFNjJSnr --- backend/models/OnboardingSilenceEpisode.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/models/OnboardingSilenceEpisode.ts b/backend/models/OnboardingSilenceEpisode.ts index f8ddb8a824..d541e9046b 100644 --- a/backend/models/OnboardingSilenceEpisode.ts +++ b/backend/models/OnboardingSilenceEpisode.ts @@ -31,8 +31,9 @@ import mongoose, { Document, Model, Schema, Types } from 'mongoose'; * `noneEnqueued` in particular degrades from a fact into an inference the * moment anything else is enqueued there. * - * And the rows themselves are not permanent — they age out at 168h. "Outlives - * the deletion" is the claim, not "outlives retention". + * And the AgentEvent rows themselves are not permanent: they age out at 168h. + * "Outlives the deletion" is the claim, not "outlives retention". Episodes + * have no TTL; only syntheticAccountPurgeService deletes them. * * So: capture at fire time, because the queue can distinguish two failures that * look identical afterwards and need opposite fixes: