@nelsonlongxiang/dsh-open-a2a-net 0.6.42 → 0.6.43

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.
package/lib/client.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","names":["css","Button","IconGlobeOutlineRegular","_Fragment"],"sources":["../../../../node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.mjs","client/A2aControl.js","client/locales.js","client/index.js"],"sourcesContent":["function r(e){var t,f,n=\"\";if(\"string\"==typeof e||\"number\"==typeof e)n+=e;else if(\"object\"==typeof e)if(Array.isArray(e)){var o=e.length;for(t=0;t<o;t++)e[t]&&(f=r(e[t]))&&(n&&(n+=\" \"),n+=f)}else for(f in e)e[f]&&(n&&(n+=\" \"),n+=f);return n}export function clsx(){for(var e,t,f=0,n=\"\",o=arguments.length;f<o;f++)(e=arguments[f])&&(t=r(e))&&(n&&(n+=\" \"),n+=t);return n}export default clsx;","import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from \"react/jsx-runtime\";\n/**\n * A2A network control: a footer action opening this host's recent network\n * activity feed. The feed is the panel's whole content — in-flight routes,\n * owed receipts, and the routing activity ring. Session/team management\n * lives in the planning view (design: docs/design/panel-activity-feed.md):\n * the stage links row above the feed is the explicit navigation to it.\n * Probes the host's state route at registration time (the seat renders only\n * on a host that serves it). While open, the feed polls so it visibly\n * follows each turn; the closed trigger's unread badge rides a slow poll.\n */\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport clsx from 'clsx';\nimport { Button, IconGlobeOutlineRegular } from '@deepseek-ai/dsh-client-ui-primitives';\nimport css from './A2aControl.module.css';\n/** Fetch the host's network state; undefined means the route is absent. */\nasync function fetchState() {\n const response = await fetch('/__dsh_a2a/state', { cache: 'no-store' });\n if (!response.ok)\n return undefined;\n const body = await response.json();\n if (body.nodes !== true || !Array.isArray(body.sessions))\n return undefined;\n return {\n sessions: body.sessions,\n activity: Array.isArray(body.activity) ? body.activity : [],\n inFlight: Array.isArray(body.inFlight) ? body.inFlight : [],\n tasks: Array.isArray(body.tasks) ? body.tasks : [],\n ...(typeof body.tasksStaleCount === 'number' ? { tasksStaleCount: body.tasksStaleCount } : {}),\n ...(typeof body.version === 'string' ? { version: body.version } : {}),\n };\n}\n/** Compact relative time for an activity row. */\nfunction relativeTime(ts) {\n const seconds = Math.max(0, Math.round((Date.now() - ts) / 1000));\n if (seconds < 60)\n return `${String(seconds)}s`;\n const minutes = Math.round(seconds / 60);\n if (minutes < 60)\n return `${String(minutes)}m`;\n return `${String(Math.round(minutes / 60))}h`;\n}\n/** One feed row: direction glyph (aria-labelled), team, peer host, age, fail mark. */\nfunction FeedRow({ dir, team, peer, ts, ok, target, openSession, t }) {\n const fresh = Date.now() - ts < 5_000;\n return (_jsxs(\"div\", { className: clsx(css.activityRow, fresh && css.fresh), \"data-ok\": ok === undefined ? undefined : String(ok), \"data-jump\": target !== undefined, title: target !== undefined ? t('a2a.jump') : undefined, onClick: () => { if (target !== undefined)\n openSession(target.id); }, role: target !== undefined ? 'button' : undefined, children: [_jsx(\"span\", { className: clsx(css.activityDir, dir), \"aria-label\": dir === 'fwd' ? 'forwarded' : dir === 'in' ? 'inbound' : 'outbound', children: dir === 'fwd' ? '⇢' : dir === 'in' ? '←' : '→' }), _jsx(\"span\", { className: css.activityTeam, title: team, children: team }), _jsx(\"span\", { className: css.activityPeer, title: peer, children: peer === '' || peer === 'local' ? '' : peer.replace(/^https?:\\/\\//, '').replace(/\\/.*$/, '') }), ok === false ? _jsx(\"span\", { className: css.failMark, \"aria-label\": \"failed\", children: \"\\u2715\" }) : null, _jsx(\"span\", { className: css.activityTime, children: relativeTime(ts) })] }));\n}\n/**\n * Render the network toggle and its activity feed.\n * @param props - composed slot props.\n * @returns the control element tree.\n */\nexport function A2aControl({ wide, t, openSession }) {\n const [open, setOpen] = useState(false);\n const [state, setState] = useState({ sessions: [], activity: [], inFlight: [], tasks: [] });\n const [seenActivity, setSeenActivity] = useState(0);\n const stopped = useRef(false);\n useEffect(() => () => { stopped.current = true; }, []);\n const wrap = useRef(null);\n // The popover anchors to the trigger's viewport rect: the sidebar column\n // clips its descendants (the collapse slide), so an in-column absolute\n // popover is cut at the column edge whenever the column is narrower than\n // the popover; fixed positioning escapes the clip at rest (the ui-settings\n // panel precedent). The left edge clamps so a narrow viewport never hides\n // the popover's right side.\n const [anchor, setAnchor] = useState(null);\n // The unread badge counts inbound activity entries that arrived while the\n // panel was closed; opening the panel clears it.\n const unread = Math.max(0, state.activity.filter(entry => entry.dir === 'in').length - seenActivity);\n const toggleOpen = () => {\n if (open) {\n setOpen(false);\n return;\n }\n const rect = wrap.current?.getBoundingClientRect();\n if (rect !== undefined) {\n setAnchor({\n left: Math.max(8, Math.min(rect.left, window.innerWidth - 336)),\n bottom: window.innerHeight - rect.top + 8,\n });\n }\n setSeenActivity(state.activity.filter(entry => entry.dir === 'in').length);\n setOpen(true);\n };\n const refresh = useCallback(() => {\n void fetchState().then((next) => {\n if (!stopped.current && next !== undefined)\n setState(next);\n }).catch(() => { });\n }, []);\n useEffect(() => {\n if (open)\n refresh();\n }, [open, refresh]);\n useEffect(() => {\n // The unread badge lives on the closed trigger, so the state must move\n // even while the panel is closed — a slow poll keeps it fresh; the open\n // panel's faster poll below takes over while visible.\n const slow = setInterval(refresh, 10_000);\n refresh();\n return () => { clearInterval(slow); };\n }, [refresh]);\n useEffect(() => {\n if (!open)\n return;\n // While the feed is open, it polls so the ring visibly follows turns.\n const poll = setInterval(refresh, 2_000);\n return () => { clearInterval(poll); };\n }, [open, refresh]);\n useEffect(() => {\n if (!open)\n return;\n const onPointerDown = (event) => {\n if (event.target.closest(`.${css.wrap}`) === null)\n setOpen(false);\n };\n document.addEventListener('pointerdown', onPointerDown);\n return () => { document.removeEventListener('pointerdown', onPointerDown); };\n }, [open]);\n // 0.6.10: the receive face of click-to-reach (0.6.7 BroadcastChannel,\n // 0.6.8 ?dsh-a2a-open deep link) is removed by ruling — 禁止任何跳转,\n // 全部使用响应式布局. The workbench's 快派 form is the only cross-surface\n // reach primitive and it stays on the workbench page; this panel's\n // FeedRow openSession remains (it switches a conversation in place inside\n // the GUI — no navigation).\n const { sessions, activity, inFlight, tasks } = state;\n return (_jsxs(\"div\", { ref: wrap, className: clsx(css.wrap, wide ? css.wide : css.rail), children: [_jsx(Button, { variant: \"ghost\", size: \"sm\", title: t('a2a.label'), \"aria-label\": t('a2a.label'), \"aria-expanded\": open, onClick: () => { toggleOpen(); }, children: _jsxs(\"span\", { className: css.trigger, children: [_jsx(IconGlobeOutlineRegular, {}), wide ? _jsx(\"span\", { children: t('a2a.label') }) : null, unread > 0 ? _jsx(\"span\", { className: css.unreadBadge, \"aria-label\": String(unread), children: unread > 9 ? '9+' : String(unread) }) : null] }) }), open\n ? (_jsxs(\"div\", { className: css.popover, role: \"dialog\", \"aria-label\": t('a2a.title'), style: anchor === null ? undefined : { left: `${String(anchor.left)}px`, bottom: `${String(anchor.bottom)}px` }, children: [_jsxs(\"div\", { className: css.title, children: [t('a2a.title'), state.version !== undefined ? _jsxs(\"span\", { className: css.versionTag, title: state.version, children: [\"v\", state.version] }) : null] }), _jsxs(\"div\", { className: css.stageRow, children: [_jsx(\"span\", { className: css.stageTitle, children: t('a2a.stageTitle') }), _jsx(\"span\", { className: css.stageLinks, children: _jsx(\"a\", { className: css.stageLink, href: \"/__dsh_a2a_nexus/\", target: \"_blank\", rel: \"noreferrer\", title: t('a2a.stageWorkbench'), children: t('a2a.stageWorkbench') }) })] }), _jsxs(\"div\", { \"aria-live\": \"polite\", children: [inFlight.length > 0 && (_jsx(\"div\", { className: css.inFlightList, \"aria-label\": t('a2a.inFlight'), children: inFlight.map((route) => (_jsxs(\"div\", { className: clsx(css.inFlightRow, Date.now() - route.startedAt > 120_000 && css.inFlightStale), title: Date.now() - route.startedAt > 120_000 ? t('a2a.inFlightStale') : undefined, children: [_jsx(\"span\", { className: css.inFlightPulse, \"aria-hidden\": true }), _jsx(FeedRow, { dir: \"out\", team: route.team, peer: route.peer, ts: route.startedAt, openSession: openSession, t: t })] }, `${route.team}-${String(route.startedAt)}`))) })), (() => {\n // C-ruling: render only settleable debt; the stale tail is a\n // count line, never a row-per-noise listing.\n const settleableTasks = tasks.filter(task => task.settleable !== false);\n const staleCount = state.tasksStaleCount ?? (tasks.length - settleableTasks.length);\n if (settleableTasks.length === 0 && staleCount === 0)\n return null;\n return (_jsxs(_Fragment, { children: [settleableTasks.length > 0 && (_jsxs(_Fragment, { children: [_jsx(\"div\", { className: css.sectionTitle, children: t('a2a.tasks') }), _jsx(\"div\", { className: css.inFlightList, \"aria-label\": t('a2a.tasks'), children: settleableTasks.map((task) => (_jsx(\"div\", { className: css.inFlightRow, title: t('a2a.tasksNote'), children: _jsx(FeedRow, { dir: \"out\", team: task.team, peer: task.peer, ts: task.startedAt, openSession: openSession, t: t }) }, task.taskId))) })] })), staleCount > 0 && (_jsxs(\"div\", { className: css.staleNote, title: t('a2a.tasksNote'), children: [t('a2a.tasksStale'), \" \\u00B7 \", String(staleCount)] }))] }));\n })(), _jsx(\"div\", { className: css.sectionTitle, children: t('a2a.activity') }), activity.length === 0\n ? _jsx(\"div\", { className: css.empty, children: t('a2a.activityEmpty') })\n : (_jsx(\"div\", { className: css.activityList, children: [...activity].reverse().slice(0, 10).map((entry, index) => {\n // A same-host session team resolves to its session row;\n // clicking the activity line jumps there (the linkage\n // into an ongoing collaboration).\n const target = sessions.find(row => row.team === entry.team);\n return (_jsx(FeedRow, { dir: entry.dir, team: entry.team, peer: entry.peer, ts: entry.ts, ok: entry.ok, ...(target !== undefined ? { target } : {}), openSession: openSession, t: t }, `${String(entry.ts)}-${String(index)}`));\n }) }))] })] }))\n : null] }));\n}\n","/** Locale dictionaries for the A2A network sidebar control (namespace `a2aNet`). */\n/** Simplified Chinese dictionary. */\nexport const zh = {\n 'a2a.label': 'A2A 网络',\n 'a2a.title': 'A2A 网络动态',\n 'a2a.activity': '最近动态',\n 'a2a.activityEmpty': '暂无网络动态——加入网络即可开始协作',\n 'a2a.inFlight': '进行中的路由',\n 'a2a.inFlightStale': '等待回执超时(对端处理慢或回执丢失);180 秒后自动解除并按已送达处理',\n 'a2a.tasks': '欠回执的异步任务',\n 'a2a.tasksNote': '异步欠账分三段:未决(等回执)→ 死信(超期自动标注,目标恢复后仍可补回执结算)→ 归档(结算留痕不蒸发)',\n 'a2a.tasksStale': '对话式已送达未答(非欠账,24h 自动死信)',\n 'a2a.jump': '点击跳转到该会话',\n 'a2a.stageTitle': 'A2A 工作台(目录 · 台账 · 活动 · 规划)',\n 'a2a.stageWorkbench': '打开工作台',\n};\n/** English dictionary. */\nexport const en = {\n 'a2a.label': 'A2A network',\n 'a2a.title': 'A2A network activity',\n 'a2a.activity': 'Recent activity',\n 'a2a.activityEmpty': 'No network activity yet — join sessions to the network to start collaborating',\n 'a2a.inFlight': 'Routes in flight',\n 'a2a.inFlightStale': 'Reply wait past 120s (slow or lost receipt); auto-releases as delivered at 180s',\n 'a2a.tasks': 'Owed receipts',\n 'a2a.tasksNote': 'Async debts in three tiers: pending (awaiting receipt) → dead-lettered (auto-flagged past the stale TTL; a revived target can still settle late) → archived (settled records kept for audit)',\n 'a2a.tasksStale': 'Delivered conversationally, no formal receipt (not debt; auto-dead-letters at the TTL)',\n 'a2a.jump': 'Click to open this session',\n 'a2a.stageTitle': 'A2A workbench (directory · ledger · activity · plan)',\n 'a2a.stageWorkbench': 'Open workbench',\n};\n","import { A2aControl } from \"./A2aControl.js\";\nimport { en, zh } from \"./locales.js\";\n/** Dictionary namespace owned by this plugin (network control copy). */\nconst NS = 'a2aNet';\n/** Services required by the browser half: the slot system, the sessions\n * flow used to wake cold rows, and the locale dictionaries. */\nexport const inject = ['slots', 'sessions', 'locale'];\n/** Registers the network control's dictionaries and its sidebar seat.\n * @param ctx - Client root context.\n */\nexport function apply(ctx) {\n ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'a2a-net: dictionaries');\n // The network control renders only on a host that serves the session-node\n // state route (a2a host half + session nodes composed); the probe keeps\n // the seat quiet everywhere else.\n void fetch('/__dsh_a2a/state', { cache: 'no-store' })\n .then(response => (response.ok ? response.json() : undefined))\n .then((info) => {\n if (info?.nodes !== true)\n return;\n ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register({\n name: 'sidebar.footer.action',\n id: 'a2a',\n order: 10,\n locale: NS,\n // Wake = the standard session-open flow: the host materializes the\n // session's agent and the a2a host half's persisted intent remounts\n // the node, so the cold row flips live on the next poll.\n inject: () => ({\n openSession: (id) => { ctx.sessions.open(id); },\n }),\n }, A2aControl));\n })\n .catch(() => { });\n}\n"],"x_google_ignoreList":[0],"mappings":"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"}
1
+ {"version":3,"file":"client.js","names":["css","Button","IconGlobeOutlineRegular","_Fragment"],"sources":["../node_modules/clsx/dist/clsx.mjs","client/A2aControl.js","client/locales.js","client/index.js"],"sourcesContent":["function r(e){var t,f,n=\"\";if(\"string\"==typeof e||\"number\"==typeof e)n+=e;else if(\"object\"==typeof e)if(Array.isArray(e)){var o=e.length;for(t=0;t<o;t++)e[t]&&(f=r(e[t]))&&(n&&(n+=\" \"),n+=f)}else for(f in e)e[f]&&(n&&(n+=\" \"),n+=f);return n}export function clsx(){for(var e,t,f=0,n=\"\",o=arguments.length;f<o;f++)(e=arguments[f])&&(t=r(e))&&(n&&(n+=\" \"),n+=t);return n}export default clsx;","import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from \"react/jsx-runtime\";\n/**\n * A2A network control: a footer action opening this host's recent network\n * activity feed. The feed is the panel's whole content — in-flight routes,\n * owed receipts, and the routing activity ring. Session/team management\n * lives in the planning view (design: docs/design/panel-activity-feed.md):\n * the stage links row above the feed is the explicit navigation to it.\n * Probes the host's state route at registration time (the seat renders only\n * on a host that serves it). While open, the feed polls so it visibly\n * follows each turn; the closed trigger's unread badge rides a slow poll.\n */\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport clsx from 'clsx';\nimport { Button, IconGlobeOutlineRegular } from '@deepseek-ai/dsh-client-ui-primitives';\nimport css from './A2aControl.module.css';\n/** Fetch the host's network state; undefined means the route is absent. */\nasync function fetchState() {\n const response = await fetch('/__dsh_a2a/state', { cache: 'no-store' });\n if (!response.ok)\n return undefined;\n const body = await response.json();\n if (body.nodes !== true || !Array.isArray(body.sessions))\n return undefined;\n return {\n sessions: body.sessions,\n activity: Array.isArray(body.activity) ? body.activity : [],\n inFlight: Array.isArray(body.inFlight) ? body.inFlight : [],\n tasks: Array.isArray(body.tasks) ? body.tasks : [],\n ...(typeof body.tasksStaleCount === 'number' ? { tasksStaleCount: body.tasksStaleCount } : {}),\n ...(typeof body.version === 'string' ? { version: body.version } : {}),\n };\n}\n/** Compact relative time for an activity row. */\nfunction relativeTime(ts) {\n const seconds = Math.max(0, Math.round((Date.now() - ts) / 1000));\n if (seconds < 60)\n return `${String(seconds)}s`;\n const minutes = Math.round(seconds / 60);\n if (minutes < 60)\n return `${String(minutes)}m`;\n return `${String(Math.round(minutes / 60))}h`;\n}\n/** One feed row: direction glyph (aria-labelled), team, peer host, age, fail mark. */\nfunction FeedRow({ dir, team, peer, ts, ok, target, openSession, t }) {\n const fresh = Date.now() - ts < 5_000;\n return (_jsxs(\"div\", { className: clsx(css.activityRow, fresh && css.fresh), \"data-ok\": ok === undefined ? undefined : String(ok), \"data-jump\": target !== undefined, title: target !== undefined ? t('a2a.jump') : undefined, onClick: () => { if (target !== undefined)\n openSession(target.id); }, role: target !== undefined ? 'button' : undefined, children: [_jsx(\"span\", { className: clsx(css.activityDir, dir), \"aria-label\": dir === 'fwd' ? 'forwarded' : dir === 'in' ? 'inbound' : 'outbound', children: dir === 'fwd' ? '⇢' : dir === 'in' ? '←' : '→' }), _jsx(\"span\", { className: css.activityTeam, title: team, children: team }), _jsx(\"span\", { className: css.activityPeer, title: peer, children: peer === '' || peer === 'local' ? '' : peer.replace(/^https?:\\/\\//, '').replace(/\\/.*$/, '') }), ok === false ? _jsx(\"span\", { className: css.failMark, \"aria-label\": \"failed\", children: \"\\u2715\" }) : null, _jsx(\"span\", { className: css.activityTime, children: relativeTime(ts) })] }));\n}\n/**\n * Render the network toggle and its activity feed.\n * @param props - composed slot props.\n * @returns the control element tree.\n */\nexport function A2aControl({ wide, t, openSession }) {\n const [open, setOpen] = useState(false);\n const [state, setState] = useState({ sessions: [], activity: [], inFlight: [], tasks: [] });\n const [seenActivity, setSeenActivity] = useState(0);\n const stopped = useRef(false);\n useEffect(() => () => { stopped.current = true; }, []);\n const wrap = useRef(null);\n // The popover anchors to the trigger's viewport rect: the sidebar column\n // clips its descendants (the collapse slide), so an in-column absolute\n // popover is cut at the column edge whenever the column is narrower than\n // the popover; fixed positioning escapes the clip at rest (the ui-settings\n // panel precedent). The left edge clamps so a narrow viewport never hides\n // the popover's right side.\n const [anchor, setAnchor] = useState(null);\n // The unread badge counts inbound activity entries that arrived while the\n // panel was closed; opening the panel clears it.\n const unread = Math.max(0, state.activity.filter(entry => entry.dir === 'in').length - seenActivity);\n const toggleOpen = () => {\n if (open) {\n setOpen(false);\n return;\n }\n const rect = wrap.current?.getBoundingClientRect();\n if (rect !== undefined) {\n setAnchor({\n left: Math.max(8, Math.min(rect.left, window.innerWidth - 336)),\n bottom: window.innerHeight - rect.top + 8,\n });\n }\n setSeenActivity(state.activity.filter(entry => entry.dir === 'in').length);\n setOpen(true);\n };\n const refresh = useCallback(() => {\n void fetchState().then((next) => {\n if (!stopped.current && next !== undefined)\n setState(next);\n }).catch(() => { });\n }, []);\n useEffect(() => {\n if (open)\n refresh();\n }, [open, refresh]);\n useEffect(() => {\n // The unread badge lives on the closed trigger, so the state must move\n // even while the panel is closed — a slow poll keeps it fresh; the open\n // panel's faster poll below takes over while visible.\n const slow = setInterval(refresh, 10_000);\n refresh();\n return () => { clearInterval(slow); };\n }, [refresh]);\n useEffect(() => {\n if (!open)\n return;\n // While the feed is open, it polls so the ring visibly follows turns.\n const poll = setInterval(refresh, 2_000);\n return () => { clearInterval(poll); };\n }, [open, refresh]);\n useEffect(() => {\n if (!open)\n return;\n const onPointerDown = (event) => {\n if (event.target.closest(`.${css.wrap}`) === null)\n setOpen(false);\n };\n document.addEventListener('pointerdown', onPointerDown);\n return () => { document.removeEventListener('pointerdown', onPointerDown); };\n }, [open]);\n // 0.6.10: the receive face of click-to-reach (0.6.7 BroadcastChannel,\n // 0.6.8 ?dsh-a2a-open deep link) is removed by ruling — 禁止任何跳转,\n // 全部使用响应式布局. The workbench's 快派 form is the only cross-surface\n // reach primitive and it stays on the workbench page; this panel's\n // FeedRow openSession remains (it switches a conversation in place inside\n // the GUI — no navigation).\n const { sessions, activity, inFlight, tasks } = state;\n return (_jsxs(\"div\", { ref: wrap, className: clsx(css.wrap, wide ? css.wide : css.rail), children: [_jsx(Button, { variant: \"ghost\", size: \"sm\", title: t('a2a.label'), \"aria-label\": t('a2a.label'), \"aria-expanded\": open, onClick: () => { toggleOpen(); }, children: _jsxs(\"span\", { className: css.trigger, children: [_jsx(IconGlobeOutlineRegular, {}), wide ? _jsx(\"span\", { children: t('a2a.label') }) : null, unread > 0 ? _jsx(\"span\", { className: css.unreadBadge, \"aria-label\": String(unread), children: unread > 9 ? '9+' : String(unread) }) : null] }) }), open\n ? (_jsxs(\"div\", { className: css.popover, role: \"dialog\", \"aria-label\": t('a2a.title'), style: anchor === null ? undefined : { left: `${String(anchor.left)}px`, bottom: `${String(anchor.bottom)}px` }, children: [_jsxs(\"div\", { className: css.title, children: [t('a2a.title'), state.version !== undefined ? _jsxs(\"span\", { className: css.versionTag, title: state.version, children: [\"v\", state.version] }) : null] }), _jsxs(\"div\", { className: css.stageRow, children: [_jsx(\"span\", { className: css.stageTitle, children: t('a2a.stageTitle') }), _jsx(\"span\", { className: css.stageLinks, children: _jsx(\"a\", { className: css.stageLink, href: \"/__dsh_a2a_nexus/\", target: \"_blank\", rel: \"noreferrer\", title: t('a2a.stageWorkbench'), children: t('a2a.stageWorkbench') }) })] }), _jsxs(\"div\", { \"aria-live\": \"polite\", children: [inFlight.length > 0 && (_jsx(\"div\", { className: css.inFlightList, \"aria-label\": t('a2a.inFlight'), children: inFlight.map((route) => (_jsxs(\"div\", { className: clsx(css.inFlightRow, Date.now() - route.startedAt > 120_000 && css.inFlightStale), title: Date.now() - route.startedAt > 120_000 ? t('a2a.inFlightStale') : undefined, children: [_jsx(\"span\", { className: css.inFlightPulse, \"aria-hidden\": true }), _jsx(FeedRow, { dir: \"out\", team: route.team, peer: route.peer, ts: route.startedAt, openSession: openSession, t: t })] }, `${route.team}-${String(route.startedAt)}`))) })), (() => {\n // C-ruling: render only settleable debt; the stale tail is a\n // count line, never a row-per-noise listing.\n const settleableTasks = tasks.filter(task => task.settleable !== false);\n const staleCount = state.tasksStaleCount ?? (tasks.length - settleableTasks.length);\n if (settleableTasks.length === 0 && staleCount === 0)\n return null;\n return (_jsxs(_Fragment, { children: [settleableTasks.length > 0 && (_jsxs(_Fragment, { children: [_jsx(\"div\", { className: css.sectionTitle, children: t('a2a.tasks') }), _jsx(\"div\", { className: css.inFlightList, \"aria-label\": t('a2a.tasks'), children: settleableTasks.map((task) => (_jsx(\"div\", { className: css.inFlightRow, title: t('a2a.tasksNote'), children: _jsx(FeedRow, { dir: \"out\", team: task.team, peer: task.peer, ts: task.startedAt, openSession: openSession, t: t }) }, task.taskId))) })] })), staleCount > 0 && (_jsxs(\"div\", { className: css.staleNote, title: t('a2a.tasksNote'), children: [t('a2a.tasksStale'), \" \\u00B7 \", String(staleCount)] }))] }));\n })(), _jsx(\"div\", { className: css.sectionTitle, children: t('a2a.activity') }), activity.length === 0\n ? _jsx(\"div\", { className: css.empty, children: t('a2a.activityEmpty') })\n : (_jsx(\"div\", { className: css.activityList, children: [...activity].reverse().slice(0, 10).map((entry, index) => {\n // A same-host session team resolves to its session row;\n // clicking the activity line jumps there (the linkage\n // into an ongoing collaboration).\n const target = sessions.find(row => row.team === entry.team);\n return (_jsx(FeedRow, { dir: entry.dir, team: entry.team, peer: entry.peer, ts: entry.ts, ok: entry.ok, ...(target !== undefined ? { target } : {}), openSession: openSession, t: t }, `${String(entry.ts)}-${String(index)}`));\n }) }))] })] }))\n : null] }));\n}\n","/** Locale dictionaries for the A2A network sidebar control (namespace `a2aNet`). */\n/** Simplified Chinese dictionary. */\nexport const zh = {\n 'a2a.label': 'A2A 网络',\n 'a2a.title': 'A2A 网络动态',\n 'a2a.activity': '最近动态',\n 'a2a.activityEmpty': '暂无网络动态——加入网络即可开始协作',\n 'a2a.inFlight': '进行中的路由',\n 'a2a.inFlightStale': '等待回执超时(对端处理慢或回执丢失);180 秒后自动解除并按已送达处理',\n 'a2a.tasks': '欠回执的异步任务',\n 'a2a.tasksNote': '异步欠账分三段:未决(等回执)→ 死信(超期自动标注,目标恢复后仍可补回执结算)→ 归档(结算留痕不蒸发)',\n 'a2a.tasksStale': '对话式已送达未答(非欠账,24h 自动死信)',\n 'a2a.jump': '点击跳转到该会话',\n 'a2a.stageTitle': 'A2A 工作台(目录 · 台账 · 活动 · 规划)',\n 'a2a.stageWorkbench': '打开工作台',\n};\n/** English dictionary. */\nexport const en = {\n 'a2a.label': 'A2A network',\n 'a2a.title': 'A2A network activity',\n 'a2a.activity': 'Recent activity',\n 'a2a.activityEmpty': 'No network activity yet — join sessions to the network to start collaborating',\n 'a2a.inFlight': 'Routes in flight',\n 'a2a.inFlightStale': 'Reply wait past 120s (slow or lost receipt); auto-releases as delivered at 180s',\n 'a2a.tasks': 'Owed receipts',\n 'a2a.tasksNote': 'Async debts in three tiers: pending (awaiting receipt) → dead-lettered (auto-flagged past the stale TTL; a revived target can still settle late) → archived (settled records kept for audit)',\n 'a2a.tasksStale': 'Delivered conversationally, no formal receipt (not debt; auto-dead-letters at the TTL)',\n 'a2a.jump': 'Click to open this session',\n 'a2a.stageTitle': 'A2A workbench (directory · ledger · activity · plan)',\n 'a2a.stageWorkbench': 'Open workbench',\n};\n","import { A2aControl } from \"./A2aControl.js\";\nimport { en, zh } from \"./locales.js\";\n/** Dictionary namespace owned by this plugin (network control copy). */\nconst NS = 'a2aNet';\n/** Services required by the browser half: the slot system, the sessions\n * flow used to wake cold rows, and the locale dictionaries. */\nexport const inject = ['slots', 'sessions', 'locale'];\n/** Registers the network control's dictionaries and its sidebar seat.\n * @param ctx - Client root context.\n */\nexport function apply(ctx) {\n ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'a2a-net: dictionaries');\n // The network control renders only on a host that serves the session-node\n // state route (a2a host half + session nodes composed); the probe keeps\n // the seat quiet everywhere else.\n void fetch('/__dsh_a2a/state', { cache: 'no-store' })\n .then(response => (response.ok ? response.json() : undefined))\n .then((info) => {\n if (info?.nodes !== true)\n return;\n ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register({\n name: 'sidebar.footer.action',\n id: 'a2a',\n order: 10,\n locale: NS,\n // Wake = the standard session-open flow: the host materializes the\n // session's agent and the a2a host half's persisted intent remounts\n // the node, so the cold row flips live on the next poll.\n inject: () => ({\n openSession: (id) => { ctx.sessions.open(id); },\n }),\n }, A2aControl));\n })\n .catch(() => { });\n}\n"],"x_google_ignoreList":[0],"mappings":"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"}
package/lib/config.js CHANGED
@@ -100,6 +100,7 @@ export const Config = s.object({
100
100
  * audience-bound form only. See docs/protocol/delivery-origin-auth.md §2.1.
101
101
  */
102
102
  originClaimUnboundWindow: s.boolean().default(true),
103
+ canvasLegacyBareMembers: s.boolean().default(true),
103
104
  /** Extra OriginClaim audience origins (port-mapped/proxied hosts). */
104
105
  originClaimAudiences: s.array(s.string()).default([]),
105
106
  linkKeys: s.dict(s.string()).default({}),
@@ -18,7 +18,7 @@
18
18
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
19
19
  import { join } from 'node:path';
20
20
  import { fileURLToPath } from 'node:url';
21
- import { canvasNameReserved as reservedCanvasName, isRemoteMemberRef, validRemoteMemberRef } from "../canvas-store.js";
21
+ import { canvasNameReserved as reservedCanvasName, isRemoteMemberRef, validRemoteMemberRef, legacyBareMember } from "../canvas-store.js";
22
22
  import { qualifiedCanvasId } from "../canvas-id.js";
23
23
  import { refreshPeerCard, registerCanvasNotify } from "./canvas-notify.js";
24
24
  import { CLI_HANDLE_RE } from "../cli-deposit.js";
@@ -133,6 +133,9 @@ export function registerAdminFaces(webServer, d) {
133
133
  sessions,
134
134
  groups: groupStore.list(),
135
135
  canvas: {
136
+ ...d.canvasMigrationState(),
137
+ // Raw retained slots are an inspection view, never the effective team member list.
138
+ retainedMembers: canvasStore.list().flatMap(name => canvasStore.rawMembersOf(name).filter(id => !canvasStore.membersOf(name).includes(id)).map(id => ({ team: name, member: id, suspended: canvasStore.isSuspended(name, id), records: canvasStore.suspensionOf(name, id) }))),
136
139
  teams: canvasStore.list().map(name => ({
137
140
  name,
138
141
  team: canvasTeamOf(name),
@@ -148,16 +151,17 @@ export function registerAdminFaces(webServer, d) {
148
151
  const rows = [];
149
152
  const seenLocal = new Set();
150
153
  for (const id of canvasStore.membersOf(name)) {
154
+ const legacy = legacyBareMember(id);
151
155
  if (isRemoteMemberRef(id)) {
152
156
  const localId = localSessionOfHandle(id);
153
157
  if (localId !== undefined) {
154
158
  if (seenLocal.has(localId))
155
159
  continue;
156
160
  seenLocal.add(localId);
157
- rows.push({ id: localId, team: `${config.team}/${id8(localId)}`, joined: sessionNodes.has(localId) || joinedSessions.has(localId), live: sessionNodes.has(localId) });
161
+ rows.push({ ...(legacy ? { provenance: 'legacy-inferred' } : {}), id: localId, team: `${config.team}/${id8(localId)}`, joined: sessionNodes.has(localId) || joinedSessions.has(localId), live: sessionNodes.has(localId) });
158
162
  continue;
159
163
  }
160
- rows.push({ id, team: id, remote: true, ...(CLI_HANDLE_RE.test(id) ? { cliHome: canvasStore.cliHomeOf(id) ?? null, cliHeld: cliDeposits.directoryRows().some(row => row.team === id), ...(canvasStore.wasCliHomeCleared(id) ? { cliHomeCleared: true } : {}) } : {}) });
164
+ rows.push({ ...(legacy ? { provenance: 'unknown' } : {}), id, team: id, remote: true, ...(CLI_HANDLE_RE.test(id) ? { cliHome: canvasStore.cliHomeOf(id) ?? null, cliHeld: cliDeposits.directoryRows().some(row => row.team === id), ...(canvasStore.wasCliHomeCleared(id) ? { cliHomeCleared: true } : {}) } : {}) });
161
165
  continue;
162
166
  }
163
167
  if (seenLocal.has(id))
@@ -169,7 +173,8 @@ export function registerAdminFaces(webServer, d) {
169
173
  })(),
170
174
  })),
171
175
  },
172
- host: lanIp === '' ? {} : { lanIp },
176
+ // #332: home-controlled aliases, never inferred from peer cards or a file reference.
177
+ host: { ...(lanIp !== '' ? { lanIp } : {}), originClaimAudiences: [...config.originClaimAudiences ?? []] },
173
178
  // gh#20 W-C (host rail): each peer row also carries its display
174
179
  // name and version from the warm card (absent until a fetch),
175
180
  // and the last-contact timestamp for the health lamp. W-A's
@@ -445,7 +450,16 @@ export function registerAdminFaces(webServer, d) {
445
450
  readJsonBody(req, res, (body) => {
446
451
  const id = typeof body.id === 'string' ? body.id : '';
447
452
  // gh#131: read while the session is still joined — its own handle reads as it only then.
448
- const ownHandle = id !== '' ? ownCanvasHandleOf(id) : undefined;
453
+ try {
454
+ if (id !== '')
455
+ d.revokeCanvasMember(id, 'leave');
456
+ }
457
+ catch (error) {
458
+ const payload = JSON.stringify({ ok: false, error: String(error) });
459
+ res.writeHead(503, { 'Content-Type': 'application/json' });
460
+ res.end(payload);
461
+ return;
462
+ }
449
463
  if (id !== '')
450
464
  sessionNodes.delete(id);
451
465
  joinedSessions.remove(id);
@@ -453,9 +467,6 @@ export function registerAdminFaces(webServer, d) {
453
467
  // without join consent would be a routing backdoor. Team
454
468
  // roster declarations die the same way (S2 consistency) — and so
455
469
  // does a membership a fleet edit stored by the session's handle.
456
- canvasStore.dropMember(id);
457
- if (ownHandle !== undefined)
458
- canvasStore.dropMember(ownHandle);
459
470
  teamMemberships.dropSession(id);
460
471
  const payload = JSON.stringify({ id });
461
472
  res.writeHead(200, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) });
@@ -545,6 +556,12 @@ export function registerAdminFaces(webServer, d) {
545
556
  body,
546
557
  signal: AbortSignal.timeout(15_000),
547
558
  });
559
+ // #320: a JSON body cannot turn a failed/redirected HTTP operation into success.
560
+ if (!reply.ok || reply.status < 200 || reply.status >= 300) {
561
+ void reply.body?.cancel().catch(() => { });
562
+ send(JSON.stringify({ ok: false, status: reply.status, error: `peer ${peerUrl} answered HTTP ${String(reply.status)} — fleet canvas operation failed` }));
563
+ return;
564
+ }
548
565
  const raw = await reply.text();
549
566
  try {
550
567
  const out = JSON.parse(raw);
@@ -738,72 +755,95 @@ export function registerAdminFaces(webServer, d) {
738
755
  readJsonBody(req, res, async (body) => {
739
756
  const action = body.action;
740
757
  let payload;
741
- if (action === 'cli-home-options') {
742
- const name = typeof body.name === 'string' ? body.name : '';
743
- const id = typeof body.id === 'string' ? body.id : '';
744
- if (!CLI_HANDLE_RE.test(id) || !canvasStore.membersOf(name).includes(id))
745
- payload = JSON.stringify({ ok: false, error: 'an existing canvas CLI member is required' });
746
- else {
747
- try {
748
- payload = JSON.stringify({ ok: true, ...await d.cliHomeOptions(id) });
749
- }
750
- catch {
751
- payload = JSON.stringify({ ok: false, error: 'fresh home card lookup failed' });
758
+ try {
759
+ if (action === 'restore-suspended' || action === 'delete-suspended') {
760
+ const request = body;
761
+ const name = typeof request.name === 'string' ? request.name : '', id = typeof request.id === 'string' ? request.id : '';
762
+ const out = canvasStore.regulateSuspended(action === 'restore-suspended' ? 'restore' : 'delete', name, id, request.revision, request.expected);
763
+ logger.info('canvas suspended owner operation %s %s %s: %s', action, name, id, out.ok);
764
+ payload = JSON.stringify({ ...out, ...canvasStore.state() });
765
+ }
766
+ else if (action === 'cli-home-options') {
767
+ const name = typeof body.name === 'string' ? body.name : '';
768
+ const id = typeof body.id === 'string' ? body.id : '';
769
+ if (!CLI_HANDLE_RE.test(id) || !canvasStore.membersOf(name).includes(id))
770
+ payload = JSON.stringify({ ok: false, error: 'an existing canvas CLI member is required' });
771
+ else {
772
+ try {
773
+ payload = JSON.stringify({ ok: true, ...await d.cliHomeOptions(id) });
774
+ }
775
+ catch {
776
+ payload = JSON.stringify({ ok: false, error: 'fresh home card lookup failed' });
777
+ }
752
778
  }
753
779
  }
754
- }
755
- else if (action === 'create') {
756
- const name = typeof body.name === 'string' ? body.name : '';
757
- const stored = canvasNameReserved(name) ? undefined : canvasStore.create(name);
758
- payload = JSON.stringify(stored === undefined ? { ok: false, error: 'invalid, roster-reserved name or team cap reached' } : { ok: true, name: stored, teams: canvasStore.list() });
759
- }
760
- else if (action === 'remove') {
761
- const name = typeof body.name === 'string' ? body.name : '';
762
- payload = JSON.stringify({ ok: canvasStore.remove(name), teams: canvasStore.list() });
763
- }
764
- else if (action === 'add-member') {
765
- const name = typeof body.name === 'string' ? body.name : '';
766
- const id = typeof body.id === 'string' ? body.id : '';
767
- // Dual member shape: a local session id must be a joined node;
768
- // a remote ref is a zone-qualified team handle whose shape this
769
- // gate checks and whose existence the UI picker (directory
770
- // rows) vouches for. Humans compose both via the workbench.
771
- const memberValid = isRemoteMemberRef(id)
772
- ? validRemoteMemberRef(id)
773
- : id !== '' && (sessionNodes.has(id) || joinedSessions.has(id));
774
- if (!canvasStore.hasTeam(name) && canvasNameReserved(name)) {
775
- payload = JSON.stringify({ ok: false, error: 'roster reserves this canvas name' });
780
+ else if (action === 'create') {
781
+ const name = typeof body.name === 'string' ? body.name : '';
782
+ const stored = canvasNameReserved(name) ? undefined : canvasStore.create(name);
783
+ payload = JSON.stringify(stored === undefined ? { ok: false, error: 'invalid, roster-reserved name or team cap reached' } : { ok: true, name: stored, teams: canvasStore.list() });
776
784
  }
777
- else if (name === '' || !memberValid) {
778
- payload = JSON.stringify({ ok: false, error: 'name and a member (joined session id, or a zone-qualified remote team handle like zone/a1b2c3d4) are required' });
785
+ else if (action === 'remove') {
786
+ const name = typeof body.name === 'string' ? body.name : '';
787
+ payload = JSON.stringify({ ok: canvasStore.remove(name), teams: canvasStore.list() });
779
788
  }
780
- else {
781
- const pin = await d.cliHomeForAdd(id, body.home).catch(() => ({ ok: false, error: 'linked home card lookup failed' }));
782
- if (!pin.ok)
783
- payload = JSON.stringify({ ok: false, error: pin.error });
789
+ else if (action === 'add-member') {
790
+ const name = typeof body.name === 'string' ? body.name : '';
791
+ const id = typeof body.id === 'string' ? body.id : '';
792
+ // Dual member shape: a local session id must be a joined node;
793
+ // a remote ref is a zone-qualified team handle whose shape this
794
+ // gate checks and whose existence the UI picker (directory
795
+ // rows) vouches for. Humans compose both via the workbench.
796
+ const memberValid = isRemoteMemberRef(id)
797
+ ? validRemoteMemberRef(id)
798
+ : id !== '' && (sessionNodes.has(id) || joinedSessions.has(id));
799
+ if (!canvasStore.hasTeam(name) && canvasNameReserved(name)) {
800
+ payload = JSON.stringify({ ok: false, error: 'roster reserves this canvas name' });
801
+ }
802
+ else if (name === '' || !memberValid) {
803
+ payload = JSON.stringify({ ok: false, error: 'name and a member (joined session id, or a zone-qualified remote team handle like zone/a1b2c3d4) are required' });
804
+ }
784
805
  else {
785
- canvasStore.create(name);
786
- const explicitHome = body.home !== undefined;
787
- const home = explicitHome ? pin.home : canvasStore.cliHomeOf(id) ?? pin.home;
788
- payload = JSON.stringify({ ok: canvasStore.addMember(name, id, home, { replaceHome: explicitHome }), teams: canvasStore.list(), members: canvasStore.membersOf(name) });
806
+ const pin = await d.cliHomeForAdd(id, body.home).catch(() => ({ ok: false, error: 'linked home card lookup failed' }));
807
+ if (!pin.ok)
808
+ payload = JSON.stringify({ ok: false, error: pin.error });
809
+ else {
810
+ canvasStore.create(name);
811
+ const explicitHome = body.home !== undefined;
812
+ const home = explicitHome ? pin.home : canvasStore.cliHomeOf(id) ?? pin.home;
813
+ payload = JSON.stringify({ ok: canvasStore.addMember(name, id, home, { replaceHome: explicitHome }), teams: canvasStore.list(), members: canvasStore.membersOf(name) });
814
+ }
789
815
  }
790
816
  }
791
- }
792
- else if (action === 'remove-member') {
793
- const name = typeof body.name === 'string' ? body.name : '';
794
- const id = typeof body.id === 'string' ? body.id : '';
795
- if (name === '' || id === '') {
796
- payload = JSON.stringify({ ok: false, error: 'name and id are required' });
817
+ else if (action === 'remove-member') {
818
+ const name = typeof body.name === 'string' ? body.name : '';
819
+ const id = typeof body.id === 'string' ? body.id : '';
820
+ if (name === '' || id === '') {
821
+ payload = JSON.stringify({ ok: false, error: 'name and id are required' });
822
+ }
823
+ else {
824
+ // gh#131: removing a local session removes its handle-stored slot too.
825
+ let removed = false;
826
+ try {
827
+ removed = isRemoteMemberRef(id) ? (!canvasStore.isSuspended(name, id) && canvasStore.removeMember(name, id)) : d.revokeCanvasMember(id, 'remove', name);
828
+ }
829
+ catch (error) {
830
+ payload = JSON.stringify({ ok: false, error: String(error) });
831
+ res.writeHead(503, { 'Content-Type': 'application/json' });
832
+ res.end(payload);
833
+ return;
834
+ }
835
+ payload = JSON.stringify({ ok: removed, teams: canvasStore.list(), members: canvasStore.membersOf(name) });
836
+ }
797
837
  }
798
838
  else {
799
- // gh#131: removing a local session removes its handle-stored slot too.
800
- const ownHandle = isRemoteMemberRef(id) ? undefined : ownCanvasHandleOf(id);
801
- const removed = [id, ...(ownHandle !== undefined ? [ownHandle] : [])].map(each => canvasStore.removeMember(name, each)).some(Boolean);
802
- payload = JSON.stringify({ ok: removed, teams: canvasStore.list(), members: canvasStore.membersOf(name) });
839
+ payload = JSON.stringify({ ok: false, error: 'unknown action' });
803
840
  }
804
841
  }
805
- else {
806
- payload = JSON.stringify({ ok: false, error: 'unknown action' });
842
+ catch (error) {
843
+ const failure = JSON.stringify({ ok: false, error: String(error) });
844
+ res.writeHead(503, { 'Content-Type': 'application/json' });
845
+ res.end(failure);
846
+ return;
807
847
  }
808
848
  res.writeHead(200, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) });
809
849
  res.end(payload);
package/lib/faces/wire.js CHANGED
@@ -182,7 +182,9 @@ export function registerWireFaces(webServer, d) {
182
182
  const name = typeof body.name === 'string' ? body.name : '';
183
183
  const rawId = typeof body.id === 'string' ? body.id : '';
184
184
  // gh#131: a peer names our sessions by handle — the session it names, for removal.
185
- const id = (isRemoteMemberRef(rawId) ? localSessionOfHandle(rawId) : undefined) ?? rawId;
185
+ // A bare handle may still infer for routing, never for deleting another stored slot.
186
+ const provenOwn = rawId.endsWith('@' + d.ownCanvasFingerprint);
187
+ const id = (provenOwn ? localSessionOfHandle(rawId) : undefined) ?? rawId;
186
188
  let payload;
187
189
  if (op === 'create') {
188
190
  const stored = canvasNameReserved(name) ? undefined : canvasStore.create(name);
package/lib/index.js CHANGED
@@ -30,7 +30,7 @@ export function resolveCallerLabel(input) {
30
30
  }
31
31
  import { encodePublicKey, signCard, verifyCard } from "./card.js";
32
32
  import { GroupStore } from "./group-store.js";
33
- import { CanvasStore, canvasNameReserved, isRemoteMemberRef } from "./canvas-store.js";
33
+ import { CanvasStore, canvasNameReserved, isRemoteMemberRef, legacyBareMember } from "./canvas-store.js";
34
34
  import { decodeSessionLog, extractTranscript, TRANSCRIPT_MAX_FILE_BYTES } from "./session-transcript.js";
35
35
  import { LayoutStore } from "./layout-store.js";
36
36
  import { peerLinkDisplay } from "./peer-link-display.js";
@@ -597,7 +597,7 @@ export function apply(ctx, config) {
597
597
  // Canvas teams: user-composed multi-member routing groups. A session node
598
598
  // may sit in many teams; routing to <team>/canvas/<name> resolves the
599
599
  // first live member or wakes the first cold one (member order = priority).
600
- const canvasStore = new CanvasStore(join(home, 'a2a', 'canvas.json'));
600
+ const canvasStore = new CanvasStore(join(home, 'a2a', 'canvas.json'), { legacyBareMembers: config.canvasLegacyBareMembers !== false });
601
601
  // #91 file channel: the sender host's blob store (docs/design/file-channel.md).
602
602
  const blobStore = new BlobStore(join(home, 'a2a'));
603
603
  // Spatial presentation state for the full-page stage (contract v2).
@@ -918,7 +918,13 @@ export function apply(ctx, config) {
918
918
  for (const id of joinedSessions.list()) {
919
919
  if (!isArchived(id))
920
920
  continue;
921
- const ownHandle = ownCanvasHandleOf(id, true);
921
+ try {
922
+ revokeCanvasMember(id, 'archive');
923
+ }
924
+ catch (error) {
925
+ logger.error('canvas archive suspension failed: %s', String(error));
926
+ continue;
927
+ }
922
928
  sessionNodes.delete(id);
923
929
  // Sections come back at the next mount (re-publish) if this archived reading was transient; the row stays.
924
930
  nodeBinder.suspend(id);
@@ -928,9 +934,6 @@ export function apply(ctx, config) {
928
934
  // joined session matches"). Safety without the deletion: wake refuses
929
935
  // archived intents (:804), card assembly and the state cold rows filter
930
936
  // them, the reconciler skips them, and JOIN_CAP bounds the file.
931
- canvasStore.dropMember(id);
932
- if (ownHandle !== undefined)
933
- canvasStore.dropMember(ownHandle);
934
937
  // S2 consistency: an archived node's roster declarations die with it
935
938
  // — a left node keeps no team membership.
936
939
  teamMemberships.dropSession(id);
@@ -961,7 +964,13 @@ export function apply(ctx, config) {
961
964
  continue;
962
965
  if (sessionNodes.has(id) || joinedSessions.has(id))
963
966
  continue;
964
- canvasStore.removeMember(name, id);
967
+ try {
968
+ canvasStore.removeMember(name, id);
969
+ }
970
+ catch (error) {
971
+ logger.error('canvas stale-member prune failed (%s/%s): %s', name, id, String(error));
972
+ continue;
973
+ }
965
974
  logger.info(`a2a: canvas team "${name}" dropped stale member ${id8(id)} (no live node, no join intent)`);
966
975
  }
967
976
  }
@@ -1004,7 +1013,7 @@ export function apply(ctx, config) {
1004
1013
  * @param includeArchived - count archived sessions too (the archive prune
1005
1014
  * drops an archived session's own handle).
1006
1015
  */
1007
- function localHandleResolver(includeArchived = false) {
1016
+ function localHandleResolver(includeArchived = false, count = false, legacyOverride = false) {
1008
1017
  const isArchived = includeArchived ? undefined : archivedSessionFilter();
1009
1018
  const byHandle = new Map();
1010
1019
  for (const id of new Set([...joinedSessions.list(), ...sessionNodes.keys()])) {
@@ -1021,7 +1030,20 @@ export function apply(ctx, config) {
1021
1030
  published.add(row.team);
1022
1031
  }
1023
1032
  return (ref) => {
1033
+ // #334 qualified member routing is deliberately deferred; this window is only legacy bare inference.
1034
+ if (!legacyOverride && config.canvasLegacyBareMembers === false)
1035
+ return undefined;
1024
1036
  const hits = byHandle.get(ref);
1037
+ const collision = (hits?.length ?? 0) > 1 || (hits?.length === 1 && published.has(ref));
1038
+ if (count && legacyBareMember(ref)) {
1039
+ legacyInferenceResolutions++;
1040
+ if (collision) {
1041
+ legacyCollisionWarnings++;
1042
+ if (legacyCollisionRecent.length >= 16)
1043
+ legacyCollisionRecent.shift();
1044
+ legacyCollisionRecent.push({ member: ref, at: Date.now() });
1045
+ }
1046
+ }
1025
1047
  return hits?.length === 1 && !published.has(ref) ? hits[0] : undefined;
1026
1048
  };
1027
1049
  }
@@ -1032,6 +1054,30 @@ export function apply(ctx, config) {
1032
1054
  * handle-stored membership must not outlive the session's consent).
1033
1055
  * @param id - the local session id (still joined when this is read).
1034
1056
  */
1057
+ const canvasMigrationEpoch = new Date().toISOString();
1058
+ let legacyInferenceResolutions = 0, legacyCollisionWarnings = 0;
1059
+ const legacyCollisionRecent = [];
1060
+ function revokeCanvasMember(id, reason, name) {
1061
+ if (!joinedSessions.has(id) && !sessionNodes.has(id))
1062
+ return false;
1063
+ const bare = config.team + '/' + id8(id);
1064
+ const inferred = localHandleResolver(true, false, true)(bare) === id ? bare : undefined;
1065
+ const qualified = bare + '@' + ownCanvasFingerprint;
1066
+ const sameId8 = new Set([...joinedSessions.list(), ...sessionNodes.keys()].filter(local => id8(local) === id8(id)));
1067
+ const pinned = canvasStore.ownerKeyOf(qualified);
1068
+ const proof = sameId8.size === 1 && (pinned === undefined || pinned === encodePublicKey(createPublicKey(nodePrivateKey))) ? qualified : undefined;
1069
+ return canvasStore.revokeLocal(id, proof, inferred, reason, name);
1070
+ }
1071
+ function canvasMigrationState() {
1072
+ const raw = canvasStore.list().flatMap(name => canvasStore.rawMembersOf(name).filter(legacyBareMember).map(member => ({ name, member })));
1073
+ const resolve = localHandleResolver(false, false);
1074
+ return { canvasLegacyBareMembers: config.canvasLegacyBareMembers !== false, epoch: canvasMigrationEpoch,
1075
+ legacyBareMembers: raw.length,
1076
+ legacyInferredMembers: raw.filter(r => canvasStore.membersOf(r.name).includes(r.member) && resolve(r.member) !== undefined).length,
1077
+ legacyInferenceResolutions, legacyCollisionWarnings, legacyCollisionRecent: [...legacyCollisionRecent],
1078
+ suspendedMembersCount: new Set(canvasStore.state().suspendedMembers.map(r => JSON.stringify([r.team, r.member]))).size,
1079
+ ...canvasStore.state() };
1080
+ }
1035
1081
  function ownCanvasHandleOf(id, includeArchived = false) {
1036
1082
  const handle = `${config.team}/${id8(id)}`;
1037
1083
  return localHandleResolver(includeArchived)(handle) === id ? handle : undefined;
@@ -1059,8 +1105,8 @@ export function apply(ctx, config) {
1059
1105
  * reading that depends on which peer cards are cached must not rewrite
1060
1106
  * the store (review of PR #151 P2-2).
1061
1107
  */
1062
- function canvasLocalMembersOf(name) {
1063
- const resolve = localHandleResolver();
1108
+ function canvasLocalMembersOf(name, count = false) {
1109
+ const resolve = localHandleResolver(false, count);
1064
1110
  return canvasStore.membersOf(name).map(id => (isRemoteMemberRef(id) ? resolve(id) ?? id : id));
1065
1111
  }
1066
1112
  /** The 8-char suffix of one agent's id. */
@@ -1111,7 +1157,7 @@ export function apply(ctx, config) {
1111
1157
  function canvasLiveAgent(name, skipId) {
1112
1158
  if (name === undefined)
1113
1159
  return undefined;
1114
- for (const id of canvasLocalMembersOf(name)) {
1160
+ for (const id of canvasLocalMembersOf(name, true)) {
1115
1161
  if (id === skipId)
1116
1162
  continue;
1117
1163
  const agent = sessionNodes.get(id);
@@ -1131,7 +1177,7 @@ export function apply(ctx, config) {
1131
1177
  if (name === undefined)
1132
1178
  return undefined;
1133
1179
  const isArchived = archivedSessionFilter();
1134
- return canvasLocalMembersOf(name).find(id => id !== skipId && joinedSessions.has(id) && !liveRoots.has(id) && isArchived?.(id) !== true);
1180
+ return canvasLocalMembersOf(name, true).find(id => id !== skipId && joinedSessions.has(id) && !liveRoots.has(id) && isArchived?.(id) !== true);
1135
1181
  }
1136
1182
  /**
1137
1183
  * Human-facing node facts for one session: the session title (fallback:
@@ -1596,7 +1642,7 @@ export function apply(ctx, config) {
1596
1642
  whenWebServerSettled((webServer) => {
1597
1643
  // gh#77 B1: the admin faces live in src/faces/admin.ts.
1598
1644
  registerAdminFaces(webServer, {
1599
- ctx, config, logger, home, session, lanIp, PLUGIN_VERSION, controlRoute, controlTrustedWithoutKey, readJsonBody, peerFetch, peerStore, peerLinkState, originClaimHeaders, refreshRemoteRows, joinedSessions, exposureStore, teamMemberships, cliDeposits, groupStore, canvasStore, cliHomeForAdd, cliHomeOptions, layoutStore, taskLedger, idempotencyStore, inFlightRoutes, recentActivity, teamAuditRing, reportRing, sessionNodes, liveRoots, mountSessionNode, coldJoinedIds, prewarmStatus, reconcileStatus, id8, sessionLabelOf, sessionTeamOf, ownCanvasHandleOf, localSessionOfHandle, canvasTeamsOfSession, nodeMetadataOf, canvasTeamOf, archivedSessionFilter, pruneArchivedJoins, pruneCanvasMemberships, verifyOriginClaim, callerLinkedToThisHost, cachedCardFetch, authorizationView,
1645
+ ctx, config, logger, home, session, lanIp, PLUGIN_VERSION, controlRoute, controlTrustedWithoutKey, readJsonBody, peerFetch, peerStore, peerLinkState, originClaimHeaders, refreshRemoteRows, joinedSessions, exposureStore, teamMemberships, cliDeposits, groupStore, canvasStore, revokeCanvasMember, canvasMigrationState, cliHomeForAdd, cliHomeOptions, layoutStore, taskLedger, idempotencyStore, inFlightRoutes, recentActivity, teamAuditRing, reportRing, sessionNodes, liveRoots, mountSessionNode, coldJoinedIds, prewarmStatus, reconcileStatus, id8, sessionLabelOf, sessionTeamOf, ownCanvasHandleOf, localSessionOfHandle, canvasTeamsOfSession, nodeMetadataOf, canvasTeamOf, archivedSessionFilter, pruneArchivedJoins, pruneCanvasMemberships, verifyOriginClaim, callerLinkedToThisHost, cachedCardFetch, authorizationView,
1600
1646
  indexUrl: import.meta.url, enforceRefused,
1601
1647
  get cardCache() { return cardCache; },
1602
1648
  get remoteRowsCache() { return remoteRowsCache; },