@nanmicoder/dsh-agent-teams 0.1.6 → 0.1.8
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/README.md +25 -2
- package/README_ZH.md +16 -2
- package/lib/client/ActivityPanel.js +11 -42
- package/lib/client/AgentTeamsCard.js +10 -32
- package/lib/client/activity-monitor.js +178 -0
- package/lib/client/index.js +0 -1
- package/lib/client.js +192 -62
- package/lib/client.js.map +1 -1
- package/lib/command.js +127 -0
- package/lib/index.js +20 -2
- package/lib/members.js +28 -8
- package/lib/state.js +104 -5
- package/lib/tools.js +16 -2
- package/lib/types/client/ActivityPanel.d.ts +0 -40
- package/lib/types/client/AgentTeamsCard.d.ts +1 -2
- package/lib/types/client/activity-monitor.d.ts +106 -0
- package/lib/types/command.d.ts +71 -0
- package/lib/types/index.d.ts +6 -0
- package/lib/types/members.d.ts +8 -4
- package/lib/types/state.d.ts +29 -0
- package/lib/types/types.d.ts +1 -1
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -78,7 +78,28 @@ Then ask for a team directly:
|
|
|
78
78
|
|
|
79
79
|
Team state is stored under `<workspace>/.agent-teams/`; the Web panel reads that disk truth and combines it with live sub-agent activity.
|
|
80
80
|
|
|
81
|
-
Member creation is zero-interaction by default:
|
|
81
|
+
Member creation is zero-interaction by default: a member on the captain's current LLM route snapshots that provider, model, and reasoning effort, while a member on a requested alternative route snapshots the target model's default effort; later continuations restore the resolved snapshot. Only an explicit heterogeneous-team request (for example, “backend on provider A/model X, frontend on provider B/model Y”) supplies a member-specific `provider` + `model`; there is no per-member model or reasoning prompt.
|
|
82
|
+
|
|
83
|
+
## Slash command
|
|
84
|
+
|
|
85
|
+
No “use AgentTeams” phrasing required. The plugin registers the
|
|
86
|
+
closed-namespace `/agent-teams` host command, so the Web GUI slash menu shows
|
|
87
|
+
an `agent-teams` placeholder with an input hint: pick it (or type the
|
|
88
|
+
command), describe the goal, and press Enter.
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
/agent-teams research the pricing pages of three competitors
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The line is claimed by the command pipeline and never reaches the model as
|
|
95
|
+
plain text — the handler queues one deterministic activation message as an
|
|
96
|
+
ordinary follow-up turn, so the captain protocol starts immediately. The
|
|
97
|
+
invocation is durably logged (`command/run` / `command/done`).
|
|
98
|
+
|
|
99
|
+
Surfaces without command adjudication (for example the headless CLI) get the
|
|
100
|
+
same deterministic activation through a gesture boundary: any genuine user
|
|
101
|
+
message starting with `/agent-teams` activates the protocol for the rest of
|
|
102
|
+
the text. Mid-sentence mentions stay ordinary prose.
|
|
82
103
|
|
|
83
104
|
## Configuration
|
|
84
105
|
|
|
@@ -94,7 +115,9 @@ Defaults work without extra setup. A trusted profile can override member behavio
|
|
|
94
115
|
maxMembers: 8
|
|
95
116
|
```
|
|
96
117
|
|
|
97
|
-
`memberProvider` is the sub-agent runtime backend (`spawn` / `fork`), not an LLM provider. Cross-LLM-provider routing uses the optional `provider` + `model` fields of `agent_teams_add_member`; `memberModel` is only a model default for all members.
|
|
118
|
+
`memberProvider` is the sub-agent runtime backend (`spawn` / `fork`), not an LLM provider. Cross-LLM-provider routing uses the optional `provider` + `model` fields of `agent_teams_add_member`; `memberModel` is only a model default for all members. A member on the captain's current provider/model inherits the captain's reasoning effort, while a changed provider or model automatically uses the target model's default. To request a particular effort, pass the optional `reasoning_effort` field — one of the target model's supported effort ids, or `"default"` to force the model's own default.
|
|
119
|
+
|
|
120
|
+
`slashCommand: false` disables the deterministic `/agent-teams` activation surfaces (slash command and gesture boundary), leaving the natural-language trigger as the only entry point.
|
|
98
121
|
|
|
99
122
|
## Boundaries
|
|
100
123
|
|
package/README_ZH.md
CHANGED
|
@@ -78,7 +78,19 @@ dsh web
|
|
|
78
78
|
|
|
79
79
|
团队状态保存在 `<workspace>/.agent-teams/`;Web 面板读取这份磁盘真相,并与实时子 Agent 活动合并展示。
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
成员创建默认零交互:成员沿用队长当前 LLM 路由时会快照该 provider、model 与思考强度;用户要求改用其他路由时,则快照目标模型的默认强度,成员后续续跑仍使用最终解析出的快照。只有当用户明确提出异构分工(例如“后端用 provider A/model X,前端用 provider B/model Y”)时,队长才会把对应的 `provider` + `model` 传给该成员;不会逐个弹出模型或思考强度选择。
|
|
82
|
+
|
|
83
|
+
## Slash 命令
|
|
84
|
+
|
|
85
|
+
无需再说“用 AgentTeams”。插件注册了封闭命名空间的 `/agent-teams` 宿主命令,Web GUI 的 slash 菜单会显示 `agent-teams` 占位项与输入提示:选中它(或直接输入命令)、描述目标、回车即可。
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
/agent-teams 调研三家竞品的定价页
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
这一行会被命令管线认领,**不会**以纯文本形式发给模型——处理器会以一条确定性激活消息排队一个普通跟进回合,队长协议立即启动。调用会持久化记录(`command/run` / `command/done`)。
|
|
92
|
+
|
|
93
|
+
没有命令裁决的表面(例如 headless CLI)也享有同等的确定性激活:任何以 `/agent-teams` 开头的真实用户消息,都会为其余文本激活该协议;句子中间出现的字样仍是普通文本。
|
|
82
94
|
|
|
83
95
|
## 配置
|
|
84
96
|
|
|
@@ -94,7 +106,9 @@ dsh web
|
|
|
94
106
|
maxMembers: 8
|
|
95
107
|
```
|
|
96
108
|
|
|
97
|
-
这里的 `memberProvider` 指子 Agent 的运行后端(`spawn` / `fork`),不是 LLM provider。跨 LLM provider 由 `agent_teams_add_member` 的可选 `provider` + `model` 参数表达;`memberModel`
|
|
109
|
+
这里的 `memberProvider` 指子 Agent 的运行后端(`spawn` / `fork`),不是 LLM provider。跨 LLM provider 由 `agent_teams_add_member` 的可选 `provider` + `model` 参数表达;`memberModel` 只是所有成员的模型默认覆盖。成员沿用队长当前 provider/model 时会继承队长的思考强度;provider 或 model 任一改变时会自动使用目标模型的默认档。需要指定特定强度时,可传入可选的 `reasoning_effort` 参数(目标模型支持的档位 id,或 `"default"` 表示强制使用模型自身默认档)。
|
|
110
|
+
|
|
111
|
+
`slashCommand: false` 可关闭确定性的 `/agent-teams` 激活面(slash 命令与手势边界),仅保留自然语言触发。
|
|
98
112
|
|
|
99
113
|
## 使用边界
|
|
100
114
|
|
|
@@ -18,11 +18,10 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
18
18
|
import { useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
|
|
19
19
|
import { IconBranchOutline16, IconCloseOutline16, } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
20
20
|
import { activityPanelExpandedForSession, compactDagLayout, COMPACT_DAG_NODE_HEIGHT, COMPACT_DAG_NODE_WIDTH, dependencyFocusTaskId, relatedTaskIds, usesParallelTaskGrid, } from "./activity-model.js";
|
|
21
|
+
import { getActivityMonitorTargetsSnapshot, getActivitySnapshotsSnapshot, startActivityPolling, subscribeActivityMonitorTargets, subscribeActivitySnapshots, } from "./activity-monitor.js";
|
|
21
22
|
import { ACTION_ART, LEAD_ART, memberArtUrl } from "./artwork.js";
|
|
22
23
|
import { OPEN_PANEL_EVENT } from "./AgentTeamsCard.js";
|
|
23
24
|
import css from './ActivityPanel.module.css';
|
|
24
|
-
/** Poll cadence for the host snapshot route. */
|
|
25
|
-
const POLL_MS = 1000;
|
|
26
25
|
/** Grace before the panel collapses once no team remains. */
|
|
27
26
|
const AUTOCLOSE_GRACE_MS = 2000;
|
|
28
27
|
/**
|
|
@@ -31,8 +30,6 @@ const AUTOCLOSE_GRACE_MS = 2000;
|
|
|
31
30
|
* right after load. New activity after this window auto-expands as usual.
|
|
32
31
|
*/
|
|
33
32
|
const AUTO_OPEN_SETTLE_MS = 4000;
|
|
34
|
-
/** Host route serving team snapshots. */
|
|
35
|
-
const STATE_URL = '/plugins/dsh-agent-teams/state';
|
|
36
33
|
/** Root marker shared with the panel CSS while the portal is expanded. */
|
|
37
34
|
const PANEL_OPEN_ATTRIBUTE = 'data-agent-teams-panel-open';
|
|
38
35
|
/** Initial-letter fallback for unmatched roles. */
|
|
@@ -263,14 +260,15 @@ export function ActivityPanel({ sessionsList, openSession }) {
|
|
|
263
260
|
setWasActive(false);
|
|
264
261
|
openSession(id);
|
|
265
262
|
};
|
|
266
|
-
const [teams, setTeams] = useState([]);
|
|
267
|
-
const [archivedTeams, setArchivedTeams] = useState([]);
|
|
268
263
|
const [open, setOpen] = useState(false);
|
|
269
264
|
const [openOwner, setOpenOwner] = useState();
|
|
270
265
|
const [autoOpened, setAutoOpened] = useState(false);
|
|
271
266
|
const [wasActive, setWasActive] = useState(false);
|
|
272
267
|
const [historic, setHistoric] = useState(new Map());
|
|
273
268
|
const current = useSyncExternalStore(sessionsList.subscribe, sessionsList.getSnapshot).current;
|
|
269
|
+
const monitorTargets = useSyncExternalStore(subscribeActivityMonitorTargets, getActivityMonitorTargetsSnapshot);
|
|
270
|
+
const { teams, archivedTeams } = useSyncExternalStore(subscribeActivitySnapshots, getActivitySnapshotsSnapshot);
|
|
271
|
+
const currentTargets = useMemo(() => current === undefined ? [] : monitorTargets.filter((target) => target.sessionId === current), [current, monitorTargets]);
|
|
274
272
|
const currentRef = useRef(current);
|
|
275
273
|
useEffect(() => { currentRef.current = current; }, [current]);
|
|
276
274
|
const mountedAtRef = useRef(performance.now());
|
|
@@ -299,42 +297,13 @@ export function ActivityPanel({ sessionsList, openSession }) {
|
|
|
299
297
|
return () => { root.removeAttribute(PANEL_OPEN_ATTRIBUTE); };
|
|
300
298
|
}, [expanded]);
|
|
301
299
|
useEffect(() => {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
const [liveResponse, archivedResponse] = await Promise.all([
|
|
310
|
-
fetch(STATE_URL, { cache: 'no-store' }),
|
|
311
|
-
fetch(`${STATE_URL}?archived=1`, { cache: 'no-store' }),
|
|
312
|
-
]);
|
|
313
|
-
if (liveResponse.ok) {
|
|
314
|
-
const body = (await liveResponse.json());
|
|
315
|
-
if (!cancelled && Array.isArray(body.teams))
|
|
316
|
-
setTeams(body.teams);
|
|
317
|
-
}
|
|
318
|
-
if (archivedResponse.ok) {
|
|
319
|
-
const body = (await archivedResponse.json());
|
|
320
|
-
if (!cancelled && Array.isArray(body.teams))
|
|
321
|
-
setArchivedTeams(body.teams);
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
catch {
|
|
325
|
-
// Host restarting; keep the last snapshot.
|
|
326
|
-
}
|
|
327
|
-
finally {
|
|
328
|
-
inFlight = false;
|
|
329
|
-
}
|
|
330
|
-
};
|
|
331
|
-
void tick();
|
|
332
|
-
const timer = setInterval(() => { void tick(); }, POLL_MS);
|
|
333
|
-
return () => {
|
|
334
|
-
cancelled = true;
|
|
335
|
-
clearInterval(timer);
|
|
336
|
-
};
|
|
337
|
-
}, []);
|
|
300
|
+
// Installing the plugin alone must not touch the state route. A successful
|
|
301
|
+
// AgentTeams conversation card registers the demand that reaches here.
|
|
302
|
+
if (currentTargets.length === 0)
|
|
303
|
+
return;
|
|
304
|
+
const controller = startActivityPolling(currentTargets);
|
|
305
|
+
return () => { controller.stop(); };
|
|
306
|
+
}, [currentTargets]);
|
|
338
307
|
useEffect(() => {
|
|
339
308
|
const onOpenPanel = (event) => {
|
|
340
309
|
const activeSession = currentRef.current;
|
|
@@ -10,7 +10,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
10
10
|
* session is re-opened for review).
|
|
11
11
|
* @module dsh-agent-teams/client/card
|
|
12
12
|
*/
|
|
13
|
-
import { useEffect, useMemo,
|
|
13
|
+
import { useEffect, useMemo, useSyncExternalStore } from 'react';
|
|
14
|
+
import { getActivitySnapshotsSnapshot, monitorAgentTeam, subscribeActivitySnapshots, } from "./activity-monitor.js";
|
|
14
15
|
import { LEAD_ART, memberArtUrl } from "./artwork.js";
|
|
15
16
|
import css from './AgentTeamsCard.module.css';
|
|
16
17
|
/** Window event name the floater listens for to open itself. */
|
|
@@ -29,40 +30,17 @@ function openActivityPanel(data) {
|
|
|
29
30
|
}));
|
|
30
31
|
}
|
|
31
32
|
/** Render one durable team as a compact conversation card. */
|
|
32
|
-
export function AgentTeamsCard({ node, openSession,
|
|
33
|
+
export function AgentTeamsCard({ node, openSession, sessionId }) {
|
|
33
34
|
const data = node.data;
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
// `conversation.chat.node` is session-scoped, so its framework-owned id is
|
|
36
|
+
// a stable owner even while another conversation becomes current.
|
|
37
|
+
const owner = data.captainSessionId || sessionId;
|
|
38
|
+
const { teams, archivedTeams } = useSyncExternalStore(subscribeActivitySnapshots, getActivitySnapshotsSnapshot);
|
|
36
39
|
useEffect(() => {
|
|
37
|
-
|
|
38
|
-
const tick = async () => {
|
|
39
|
-
for (const url of ['/plugins/dsh-agent-teams/state', '/plugins/dsh-agent-teams/state?archived=1']) {
|
|
40
|
-
try {
|
|
41
|
-
const response = await fetch(url, { cache: 'no-store' });
|
|
42
|
-
if (!response.ok)
|
|
43
|
-
continue;
|
|
44
|
-
const body = (await response.json());
|
|
45
|
-
const found = Array.isArray(body.teams)
|
|
46
|
-
? body.teams.find((team) => team.teamId === data.teamId && (owner === '' || team.captainSessionId === owner))
|
|
47
|
-
: undefined;
|
|
48
|
-
if (found !== undefined) {
|
|
49
|
-
if (!cancelled)
|
|
50
|
-
setSnapshot(found);
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
catch {
|
|
55
|
-
// Host restarting; retry on the next poll.
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
};
|
|
59
|
-
void tick();
|
|
60
|
-
const timer = setInterval(() => { void tick(); }, 1500);
|
|
61
|
-
return () => {
|
|
62
|
-
cancelled = true;
|
|
63
|
-
clearInterval(timer);
|
|
64
|
-
};
|
|
40
|
+
return monitorAgentTeam(owner, data.teamId);
|
|
65
41
|
}, [data.teamId, owner]);
|
|
42
|
+
const snapshot = teams.find((team) => team.teamId === data.teamId && (owner === '' || team.captainSessionId === owner))
|
|
43
|
+
?? archivedTeams.find((team) => team.teamId === data.teamId && (owner === '' || team.captainSessionId === owner));
|
|
66
44
|
const resolved = useMemo(() => ({
|
|
67
45
|
...data,
|
|
68
46
|
captainSessionId: snapshot?.captainSessionId ?? owner,
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/** Shared, demand-driven state for the AgentTeams browser monitor. */
|
|
2
|
+
const targets = new Map();
|
|
3
|
+
const targetListeners = new Set();
|
|
4
|
+
const snapshotListeners = new Set();
|
|
5
|
+
let targetSnapshot = [];
|
|
6
|
+
let activitySnapshots = { teams: [], archivedTeams: [] };
|
|
7
|
+
function targetKey(sessionId, teamId) {
|
|
8
|
+
return `${sessionId}\u0000${teamId}`;
|
|
9
|
+
}
|
|
10
|
+
function publishTargets() {
|
|
11
|
+
targetSnapshot = [...targets.values()]
|
|
12
|
+
.filter((target) => target.active)
|
|
13
|
+
.map(({ key, sessionId, teamId }) => ({ key, sessionId, teamId }));
|
|
14
|
+
for (const listener of targetListeners)
|
|
15
|
+
listener();
|
|
16
|
+
}
|
|
17
|
+
/** Subscribe to the active monitor-target list (React external-store shape). */
|
|
18
|
+
export function subscribeActivityMonitorTargets(listener) {
|
|
19
|
+
targetListeners.add(listener);
|
|
20
|
+
return () => { targetListeners.delete(listener); };
|
|
21
|
+
}
|
|
22
|
+
/** Read the stable active-target snapshot. */
|
|
23
|
+
export function getActivityMonitorTargetsSnapshot() {
|
|
24
|
+
return targetSnapshot;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Register one successful AgentTeams card as a monitoring demand.
|
|
28
|
+
*
|
|
29
|
+
* The returned cleanup is reference-counted so multiple cards and React
|
|
30
|
+
* StrictMode remounts cannot stop another card's monitor.
|
|
31
|
+
*/
|
|
32
|
+
export function monitorAgentTeam(sessionId, teamId) {
|
|
33
|
+
const owner = sessionId.trim();
|
|
34
|
+
const id = teamId.trim();
|
|
35
|
+
if (owner === '' || id === '')
|
|
36
|
+
return () => { };
|
|
37
|
+
const key = targetKey(owner, id);
|
|
38
|
+
const existing = targets.get(key);
|
|
39
|
+
if (existing === undefined) {
|
|
40
|
+
targets.set(key, { key, sessionId: owner, teamId: id, refs: 1, active: true });
|
|
41
|
+
publishTargets();
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
existing.refs += 1;
|
|
45
|
+
if (!existing.active) {
|
|
46
|
+
existing.active = true;
|
|
47
|
+
publishTargets();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
let released = false;
|
|
51
|
+
return () => {
|
|
52
|
+
if (released)
|
|
53
|
+
return;
|
|
54
|
+
released = true;
|
|
55
|
+
const current = targets.get(key);
|
|
56
|
+
if (current === undefined)
|
|
57
|
+
return;
|
|
58
|
+
current.refs -= 1;
|
|
59
|
+
if (current.refs <= 0) {
|
|
60
|
+
targets.delete(key);
|
|
61
|
+
if (current.active)
|
|
62
|
+
publishTargets();
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** Stop polling targets whose final archived snapshot has been captured. */
|
|
67
|
+
export function settleActivityMonitorTargets(keys) {
|
|
68
|
+
let changed = false;
|
|
69
|
+
for (const key of keys) {
|
|
70
|
+
const target = targets.get(key);
|
|
71
|
+
if (target?.active !== true)
|
|
72
|
+
continue;
|
|
73
|
+
target.active = false;
|
|
74
|
+
changed = true;
|
|
75
|
+
}
|
|
76
|
+
if (changed)
|
|
77
|
+
publishTargets();
|
|
78
|
+
}
|
|
79
|
+
/** Subscribe to the shared live/archive snapshot. */
|
|
80
|
+
export function subscribeActivitySnapshots(listener) {
|
|
81
|
+
snapshotListeners.add(listener);
|
|
82
|
+
return () => { snapshotListeners.delete(listener); };
|
|
83
|
+
}
|
|
84
|
+
/** Read the stable shared live/archive snapshot. */
|
|
85
|
+
export function getActivitySnapshotsSnapshot() {
|
|
86
|
+
return activitySnapshots;
|
|
87
|
+
}
|
|
88
|
+
/** Publish one or both successful state-route responses. */
|
|
89
|
+
export function updateActivitySnapshots(update) {
|
|
90
|
+
const next = {
|
|
91
|
+
teams: update.teams ?? activitySnapshots.teams,
|
|
92
|
+
archivedTeams: update.archivedTeams ?? activitySnapshots.archivedTeams,
|
|
93
|
+
};
|
|
94
|
+
if (next.teams === activitySnapshots.teams && next.archivedTeams === activitySnapshots.archivedTeams)
|
|
95
|
+
return;
|
|
96
|
+
activitySnapshots = next;
|
|
97
|
+
for (const listener of snapshotListeners)
|
|
98
|
+
listener();
|
|
99
|
+
}
|
|
100
|
+
/** Poll cadence for the live host snapshot route. */
|
|
101
|
+
export const ACTIVITY_POLL_MS = 1000;
|
|
102
|
+
/** Host route serving live and archived team snapshots. */
|
|
103
|
+
export const ACTIVITY_STATE_URL = '/plugins/dsh-agent-teams/state';
|
|
104
|
+
/**
|
|
105
|
+
* Start the single polling loop for the current session's requested targets.
|
|
106
|
+
*
|
|
107
|
+
* With no targets this is deliberately inert: installing the plugin must not
|
|
108
|
+
* touch the state route. Live state is polled at the normal cadence; archive
|
|
109
|
+
* state is fetched only as a one-time fallback for targets no longer live.
|
|
110
|
+
*/
|
|
111
|
+
export function startActivityPolling(monitorTargets, runtime = {}) {
|
|
112
|
+
if (monitorTargets.length === 0) {
|
|
113
|
+
return { firstTick: Promise.resolve(), stop: () => { } };
|
|
114
|
+
}
|
|
115
|
+
const fetchState = runtime.fetchState ?? ((url, init) => fetch(url, init));
|
|
116
|
+
const schedule = runtime.schedule ?? ((callback, intervalMs) => setInterval(callback, intervalMs));
|
|
117
|
+
const cancel = runtime.cancel ?? ((timer) => { clearInterval(timer); });
|
|
118
|
+
const publishSnapshots = runtime.publishSnapshots ?? updateActivitySnapshots;
|
|
119
|
+
const settleTargets = runtime.settleTargets ?? settleActivityMonitorTargets;
|
|
120
|
+
let cancelled = false;
|
|
121
|
+
let inFlight = false;
|
|
122
|
+
let controller;
|
|
123
|
+
const tick = async () => {
|
|
124
|
+
if (inFlight || cancelled)
|
|
125
|
+
return;
|
|
126
|
+
inFlight = true;
|
|
127
|
+
controller = new AbortController();
|
|
128
|
+
try {
|
|
129
|
+
const liveResponse = await fetchState(ACTIVITY_STATE_URL, {
|
|
130
|
+
cache: 'no-store',
|
|
131
|
+
signal: controller.signal,
|
|
132
|
+
});
|
|
133
|
+
if (!liveResponse.ok)
|
|
134
|
+
return;
|
|
135
|
+
const body = (await liveResponse.json());
|
|
136
|
+
if (cancelled || !Array.isArray(body.teams))
|
|
137
|
+
return;
|
|
138
|
+
const liveTeams = body.teams;
|
|
139
|
+
publishSnapshots({ teams: liveTeams });
|
|
140
|
+
const missing = monitorTargets.filter((target) => !liveTeams.some((team) => team.captainSessionId === target.sessionId && team.teamId === target.teamId));
|
|
141
|
+
if (missing.length === 0)
|
|
142
|
+
return;
|
|
143
|
+
// Archives are immutable. A successful fallback retires every missing
|
|
144
|
+
// target, including legacy cards whose host archive no longer exists.
|
|
145
|
+
const archivedResponse = await fetchState(`${ACTIVITY_STATE_URL}?archived=1`, {
|
|
146
|
+
cache: 'no-store',
|
|
147
|
+
signal: controller.signal,
|
|
148
|
+
});
|
|
149
|
+
if (!archivedResponse.ok)
|
|
150
|
+
return;
|
|
151
|
+
const archivedBody = (await archivedResponse.json());
|
|
152
|
+
if (cancelled || !Array.isArray(archivedBody.teams))
|
|
153
|
+
return;
|
|
154
|
+
publishSnapshots({ archivedTeams: archivedBody.teams });
|
|
155
|
+
settleTargets(new Set(missing.map((target) => target.key)));
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
if (error?.name === 'AbortError')
|
|
159
|
+
return;
|
|
160
|
+
// Host restarting; keep the last snapshot and retry on the next tick.
|
|
161
|
+
}
|
|
162
|
+
finally {
|
|
163
|
+
inFlight = false;
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
const firstTick = tick();
|
|
167
|
+
const timer = schedule(() => { void tick(); }, ACTIVITY_POLL_MS);
|
|
168
|
+
return {
|
|
169
|
+
firstTick,
|
|
170
|
+
stop: () => {
|
|
171
|
+
if (cancelled)
|
|
172
|
+
return;
|
|
173
|
+
cancelled = true;
|
|
174
|
+
controller?.abort();
|
|
175
|
+
cancel(timer);
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
package/lib/client/index.js
CHANGED