dsh-live-teams 0.1.0

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.
Files changed (69) hide show
  1. package/LICENSE +176 -0
  2. package/NOTICE +11 -0
  3. package/README.md +85 -0
  4. package/cordis.patch.yml +25 -0
  5. package/lib/binding.d.ts +18 -0
  6. package/lib/binding.js +42 -0
  7. package/lib/changed-paths.d.ts +26 -0
  8. package/lib/changed-paths.js +69 -0
  9. package/lib/client.js +6753 -0
  10. package/lib/command-queue.d.ts +60 -0
  11. package/lib/command-queue.js +185 -0
  12. package/lib/compatibility.js +109 -0
  13. package/lib/context-provider.d.ts +110 -0
  14. package/lib/context-provider.js +249 -0
  15. package/lib/dispatch.d.ts +174 -0
  16. package/lib/dispatch.js +624 -0
  17. package/lib/errors.d.ts +36 -0
  18. package/lib/errors.js +103 -0
  19. package/lib/git-artifacts.d.ts +50 -0
  20. package/lib/git-artifacts.js +242 -0
  21. package/lib/index.d.ts +14 -0
  22. package/lib/index.js +14 -0
  23. package/lib/mailbox.d.ts +274 -0
  24. package/lib/mailbox.js +721 -0
  25. package/lib/member-tools.d.ts +57 -0
  26. package/lib/member-tools.js +1265 -0
  27. package/lib/migrations.d.ts +17 -0
  28. package/lib/migrations.js +47 -0
  29. package/lib/plugin.d.ts +106 -0
  30. package/lib/plugin.js +1003 -0
  31. package/lib/roles.d.ts +35 -0
  32. package/lib/roles.js +284 -0
  33. package/lib/routes.d.ts +586 -0
  34. package/lib/routes.js +2816 -0
  35. package/lib/scope.d.ts +62 -0
  36. package/lib/scope.js +133 -0
  37. package/lib/session-bridge.d.ts +76 -0
  38. package/lib/session-bridge.js +147 -0
  39. package/lib/session-title.js +35 -0
  40. package/lib/storage.d.ts +9 -0
  41. package/lib/storage.js +65 -0
  42. package/lib/task-store.d.ts +729 -0
  43. package/lib/task-store.js +2205 -0
  44. package/lib/team-store.d.ts +216 -0
  45. package/lib/team-store.js +765 -0
  46. package/lib/tree-snapshot.d.ts +28 -0
  47. package/lib/tree-snapshot.js +80 -0
  48. package/lib/types/client/TeamView.d.ts +26 -0
  49. package/lib/types/client/TeamView.dom.test.d.ts +1 -0
  50. package/lib/types/client/api.d.ts +522 -0
  51. package/lib/types/client/api.test.d.ts +1 -0
  52. package/lib/types/client/attention.d.ts +65 -0
  53. package/lib/types/client/attention.test.d.ts +1 -0
  54. package/lib/types/client/index.d.ts +31 -0
  55. package/lib/types/client/locales.d.ts +577 -0
  56. package/lib/types/client/member-name.d.ts +14 -0
  57. package/lib/types/client/member-name.test.d.ts +1 -0
  58. package/lib/types/client/roster.d.ts +26 -0
  59. package/lib/types/client/roster.test.d.ts +1 -0
  60. package/lib/types/client/styles.d.ts +3 -0
  61. package/package.json +104 -0
  62. package/roles/builder.md +40 -0
  63. package/roles/delegate.md +36 -0
  64. package/roles/lead.md +46 -0
  65. package/roles/oracle.md +36 -0
  66. package/roles/researcher.md +37 -0
  67. package/roles/reviewer.md +45 -0
  68. package/roles/scout.md +36 -0
  69. package/roles/verifier.md +36 -0
package/lib/plugin.js ADDED
@@ -0,0 +1,1003 @@
1
+ import { ERROR_CODES, LiveTeamsError } from "./errors.js";
2
+ import { checkCompatibility } from "./compatibility.js";
3
+ import { TeamStore, readDispatchSwitch, readMemberBySessionSync, readTeamStateSync, resolveLeadMemberId } from "./team-store.js";
4
+ import { readRoleRegistry, readRoleRegistrySync } from "./roles.js";
5
+ import { createSessionBridge } from "./session-bridge.js";
6
+ import { createCommandQueue } from "./command-queue.js";
7
+ import { createMailbox } from "./mailbox.js";
8
+ import { snapshotTree } from "./tree-snapshot.js";
9
+ import { createTaskStore, routeAllowsContact } from "./task-store.js";
10
+ import { createDispatcher, projectDispatch } from "./dispatch.js";
11
+ import { registerMemberToolsIfAvailable } from "./member-tools.js";
12
+ import { createMembershipContextProvider } from "./context-provider.js";
13
+ import { gitChangedPaths, workspaceMode } from "./changed-paths.js";
14
+ import { baseInfo, commitPaths, commitPathsOf, integrateCommit, pinCommit, refTip } from "./git-artifacts.js";
15
+ import { assertSessionDescriptorInTeamWorkspace, assertSessionInTeamWorkspace } from "./binding.js";
16
+ import { applySessionTitle } from "./session-title.js";
17
+ import { registerLiveTeamsRoutes } from "./routes.js";
18
+ import path from "node:path";
19
+ import { randomUUID } from "node:crypto";
20
+ import { existsSync, readFileSync, realpathSync } from "node:fs";
21
+ import { access, copyFile, mkdir } from "node:fs/promises";
22
+ import { fileURLToPath } from "node:url";
23
+ //#region src/plugin.ts
24
+ const name = "live-teams-host";
25
+ const inject = [
26
+ "sessionController",
27
+ "sessions",
28
+ "agents",
29
+ "systemPrompt",
30
+ "workspaceRegistry"
31
+ ];
32
+ function canonicalize(value) {
33
+ try {
34
+ return realpathSync.native(value);
35
+ } catch {
36
+ return value;
37
+ }
38
+ }
39
+ function text(value) {
40
+ return typeof value === "string" && value.length > 0 ? value : void 0;
41
+ }
42
+ /**
43
+ * Mount one host instance. The optional workspacePath is a deliberate pin:
44
+ * with a pin the old single-workspace route shape is retained, while without it
45
+ * every request is resolved through its Session's cwd.
46
+ */
47
+ function apply(ctx, config = {}) {
48
+ if (config.enabled !== true) return;
49
+ const configuredPath = typeof config.workspacePath === "string" && config.workspacePath.length > 0 ? path.resolve(config.workspacePath) : void 0;
50
+ if (config.codingDispatch !== void 0 && config.codingDispatch !== "manual" && config.codingDispatch !== "automatic") ctx.logger?.warn?.(`live-teams: unknown codingDispatch "${String(config.codingDispatch)}"; coding dispatch stays manual`);
51
+ const compatibility = checkCompatibility(ctx);
52
+ if (compatibility.verdict !== "compatible") ctx.logger?.warn?.(`live-teams: ${compatibility.detail}`);
53
+ const codingDispatch = config.codingDispatch === "automatic" ? "automatic" : "manual";
54
+ const sharedPaths = Array.isArray(config.sharedPaths) ? config.sharedPaths.filter((entry) => typeof entry === "string" && entry.trim().length > 0) : [];
55
+ const globalRolesDirectory = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "roles");
56
+ let disposing = false;
57
+ ctx.effect(() => async () => {
58
+ disposing = true;
59
+ const stopping = [...bindingsByWorkspaceId.values()].map((binding) => binding.dispatcher.stop());
60
+ bindingsByWorkspaceId.clear();
61
+ await Promise.all(stopping);
62
+ }, "live-teams-host.dispose");
63
+ const bridge = createSessionBridge(ctx.agents === void 0 ? { controller: ctx.sessionController } : {
64
+ controller: ctx.sessionController,
65
+ agents: ctx.agents
66
+ });
67
+ function registryEntries() {
68
+ try {
69
+ return ctx.workspaceRegistry?.list?.() ?? [];
70
+ } catch (error) {
71
+ ctx.logger?.warn?.(`live-teams: workspace registry unavailable: ${String(error)}`);
72
+ return [];
73
+ }
74
+ }
75
+ function entryForPath(value) {
76
+ const canonical = canonicalize(value);
77
+ return registryEntries().find((entry) => typeof entry.path === "string" && canonicalize(entry.path) === canonical);
78
+ }
79
+ function workspaceTitle(entry, workspacePath, fallback) {
80
+ return text(entry?.title) ?? (fallback.length > 0 ? fallback : path.basename(workspacePath));
81
+ }
82
+ const bindingsByWorkspaceId = /* @__PURE__ */ new Map();
83
+ const sessionBindings = /* @__PURE__ */ new Map();
84
+ function bindingFor(input) {
85
+ const existing = bindingsByWorkspaceId.get(input.id);
86
+ if (existing !== void 0) return existing;
87
+ const workspacePath = canonicalize(input.path);
88
+ const store = new TeamStore({
89
+ workspacePath,
90
+ workspaceId: input.id,
91
+ teamId: input.teamId,
92
+ name: input.teamName
93
+ });
94
+ let escalationMailbox;
95
+ let dispatcher;
96
+ /** Resolves once this binding has looked for its own team under another workspace id. */
97
+ let adoption = Promise.resolve();
98
+ const taskStore = createTaskStore({
99
+ workspacePath,
100
+ teamId: input.teamId,
101
+ appendAudit: async (event) => {
102
+ await audit(event);
103
+ },
104
+ changedPaths: gitChangedPaths,
105
+ workspaceMode: async (candidate) => {
106
+ if (config.scopeMode === "git") return "git";
107
+ if (config.scopeMode === "files") return "files";
108
+ return await workspaceMode(candidate);
109
+ },
110
+ snapshotTree,
111
+ git: {
112
+ base: async (candidate) => await baseInfo(candidate),
113
+ tip: async (candidate, ref) => await refTip(candidate, ref),
114
+ commit: async (candidate, base, request) => await commitPaths(candidate, base, request),
115
+ pin: async (candidate, ref, sha) => await pinCommit(candidate, ref, sha),
116
+ integrate: async (candidate, ref, headSha, expectedTip, commitPaths) => await integrateCommit(candidate, ref, headSha, expectedTip, commitPaths),
117
+ changedBetween: async (candidate, fromSha, toSha) => await commitPathsOf(candidate, fromSha, toSha)
118
+ },
119
+ deliverEscalation: async ({ leadMemberId, taskId, submissionId, round, reviewerMemberId, problem, tried }) => {
120
+ if (escalationMailbox === void 0) return;
121
+ await escalationMailbox.sendFromScheduler({
122
+ to: leadMemberId,
123
+ kind: "message",
124
+ content: `Review round ${round.round} escalated by ${reviewerMemberId} for task ${taskId}, submission ${submissionId}. Blocking problem: ${problem}. Already tried: ${tried}`
125
+ });
126
+ },
127
+ deliverClosing: async ({ assignee, taskId, note }) => {
128
+ if (escalationMailbox === void 0) return;
129
+ await escalationMailbox.sendFromScheduler({
130
+ to: assignee,
131
+ kind: "message",
132
+ content: `The work is accepted, await the next task. Task ${taskId} is archived.${note === void 0 || note.trim().length === 0 ? "" : ` Lead note: ${note.trim()}`}`
133
+ });
134
+ },
135
+ deliverDirection: async ({ assignee, taskId, instructions }) => {
136
+ if (escalationMailbox === void 0) return;
137
+ await escalationMailbox.sendFromScheduler({
138
+ to: assignee,
139
+ kind: "direction",
140
+ content: `Lead direction for task ${taskId}: ${instructions}`
141
+ });
142
+ },
143
+ interruptSession: async (sessionId, reason) => {
144
+ const agent = ctx.agents?.get?.(sessionId);
145
+ if (agent === void 0 || agent === null) return;
146
+ await bridge.interrupt(sessionId, reason);
147
+ },
148
+ awaitQuiescence: async (sessionId, timeoutMs) => {
149
+ const deadline = Date.now() + timeoutMs;
150
+ for (;;) {
151
+ const agent = ctx.agents?.get?.(sessionId);
152
+ if (agent === void 0 || agent === null) return true;
153
+ if (agent.status !== "running") return true;
154
+ if (Date.now() >= deadline) return false;
155
+ await new Promise((resolve) => setTimeout(resolve, 100));
156
+ }
157
+ }
158
+ });
159
+ let opening;
160
+ const ensureStore = () => {
161
+ if (opening !== void 0) return opening;
162
+ opening = store.open().then(() => store);
163
+ opening.catch(() => {
164
+ opening = void 0;
165
+ });
166
+ return opening;
167
+ };
168
+ /**
169
+ * One audit sink for the whole binding: every durable state change lands in the
170
+ * team journal, and the dispatcher hears all of them. Events are only wake-up
171
+ * signals for it — every pass re-reads durable state — so a missed or a
172
+ * duplicated event can neither lose work nor duplicate it (B29).
173
+ */
174
+ async function audit(event) {
175
+ await (await ensureStore()).appendEvent(input.teamId, event);
176
+ dispatcher?.notice(event);
177
+ }
178
+ const teamStateFile = path.join(workspacePath, ".dsh-live-teams", "teams", input.teamId, "team.json");
179
+ const disabledMarker = path.join(workspacePath, ".dsh-live-teams", "teams", input.teamId, "disabled");
180
+ const enabledSync = () => {
181
+ if (!existsSync(teamStateFile)) return false;
182
+ try {
183
+ readFileSync(disabledMarker, "utf8");
184
+ return false;
185
+ } catch (error) {
186
+ if (error.code === "ENOENT") return true;
187
+ throw error;
188
+ }
189
+ };
190
+ const teamNameSync = () => {
191
+ try {
192
+ return text(JSON.parse(readFileSync(teamStateFile, "utf8")).name)?.trim() ?? input.teamName;
193
+ } catch {
194
+ return input.teamName;
195
+ }
196
+ };
197
+ const storeFacade = {
198
+ memberBySessionSync: (team, sessionId) => readMemberBySessionSync({
199
+ workspacePath,
200
+ teamId: team,
201
+ sessionId
202
+ }),
203
+ teamStateSync: (team) => readTeamStateSync({
204
+ workspacePath,
205
+ teamId: team
206
+ }),
207
+ activeTaskForMemberSync: (_team, memberId) => taskStore.activeTaskForMemberSync(memberId),
208
+ tasksSync: (_team) => taskStore.listTasksSync(),
209
+ attemptSync: (_team, taskId) => taskStore.latestAttemptForTaskSync(taskId),
210
+ memberBySession: async (team, sessionId) => (await ensureStore()).memberBySession(team, sessionId)
211
+ };
212
+ let roleRegistry;
213
+ const workspaceRolesDirectory = path.join(workspacePath, ".dsh-live-teams", "roles");
214
+ const readRoles = async () => {
215
+ roleRegistry = await readRoleRegistry(globalRolesDirectory, workspaceRolesDirectory);
216
+ return roleRegistry;
217
+ };
218
+ const resolveRoleSync = (value) => {
219
+ try {
220
+ return readRoleRegistrySync(globalRolesDirectory, workspaceRolesDirectory).resolve(value);
221
+ } catch (error) {
222
+ ctx.logger?.warn?.(`live-teams: role registry unavailable: ${String(error)}`);
223
+ return;
224
+ }
225
+ };
226
+ const contextBinding = {
227
+ store: storeFacade,
228
+ teamId: input.teamId,
229
+ teamName: teamNameSync,
230
+ workspacePath,
231
+ activeTaskForMemberSync: storeFacade.activeTaskForMemberSync,
232
+ tasksSync: storeFacade.tasksSync,
233
+ attemptSync: storeFacade.attemptSync,
234
+ enabled: enabledSync,
235
+ roleRegistry: { resolve: resolveRoleSync },
236
+ live: (member) => member.sessionId !== void 0 && ctx.agents?.get?.(member.sessionId) !== void 0
237
+ };
238
+ const titleSession = (sessionId, displayName, reason, previousTitle) => applySessionTitle({
239
+ rename: (id, title) => bridge.rename(id, title),
240
+ ...ctx.logger?.warn === void 0 ? {} : { warn: (message) => ctx.logger?.warn?.(message) }
241
+ }, {
242
+ sessionId,
243
+ displayName,
244
+ teamName: teamNameSync(),
245
+ reason,
246
+ ...previousTitle === void 0 ? {} : { previousTitle }
247
+ });
248
+ const status = async () => {
249
+ const current = await store.readExisting();
250
+ if (current === void 0) return {
251
+ enabled: false,
252
+ teamId: input.teamId,
253
+ teamName: input.teamName,
254
+ workspacePath,
255
+ memberCount: 0
256
+ };
257
+ if (!!await store.isDisabled(input.teamId)) return {
258
+ enabled: false,
259
+ teamId: input.teamId,
260
+ teamName: current.name ?? input.teamName,
261
+ workspacePath,
262
+ revision: current.revision,
263
+ memberCount: current.members.length
264
+ };
265
+ return {
266
+ enabled: true,
267
+ teamId: input.teamId,
268
+ teamName: current.name ?? input.teamName,
269
+ workspacePath,
270
+ revision: current.revision,
271
+ memberCount: current.members.length
272
+ };
273
+ };
274
+ const listTeamCandidates = async () => store.listAdoptionCandidates({
275
+ workspacePath,
276
+ derivedTeamId: input.teamId
277
+ });
278
+ const enableTeam = async (requestedName, createNew = false) => {
279
+ if (await store.readExisting() === void 0) {
280
+ const candidates = await listTeamCandidates();
281
+ if (candidates.length > 0 && !createNew) throw new LiveTeamsError(ERROR_CODES.CAPABILITY_UNAVAILABLE, "existing team found", {
282
+ auditDetail: "existing team found",
283
+ details: { candidates }
284
+ });
285
+ }
286
+ const current = requestedName === void 0 ? await store.open() : await store.open({ name: requestedName });
287
+ await store.setDisabled(input.teamId, false);
288
+ await store.appendEvent(input.teamId, {
289
+ kind: "team/enabled",
290
+ teamId: input.teamId,
291
+ name: current.name ?? requestedName ?? input.teamName,
292
+ at: Date.now(),
293
+ actor: "human:desktop"
294
+ });
295
+ return status();
296
+ };
297
+ const adoptTeam = async (candidateTeamId, replaceEmptyDerived) => {
298
+ const adopted = await store.adoptTeam({
299
+ workspacePath,
300
+ workspaceId: input.id,
301
+ derivedTeamId: input.teamId,
302
+ candidateTeamId,
303
+ replaceEmptyDerived
304
+ });
305
+ return {
306
+ status: await status(),
307
+ adoptedFrom: adopted.adoptedFrom
308
+ };
309
+ };
310
+ const disableTeam = async () => {
311
+ const current = await store.open();
312
+ await store.setDisabled(input.teamId, true);
313
+ await store.appendEvent(input.teamId, {
314
+ kind: "team/disabled",
315
+ teamId: input.teamId,
316
+ at: Date.now(),
317
+ actor: "human:desktop"
318
+ });
319
+ return {
320
+ enabled: false,
321
+ teamId: input.teamId,
322
+ teamName: current.name ?? input.teamName,
323
+ workspacePath,
324
+ memberCount: 0
325
+ };
326
+ };
327
+ const renameTeam = async (requestedName, expectedRevision) => {
328
+ const renamed = await store.renameTeam(input.teamId, expectedRevision, requestedName);
329
+ await store.appendEvent(input.teamId, {
330
+ kind: "team/renamed",
331
+ teamId: input.teamId,
332
+ from: renamed.from,
333
+ to: requestedName,
334
+ at: Date.now(),
335
+ actor: "human:desktop"
336
+ });
337
+ return {
338
+ from: renamed.from,
339
+ status: {
340
+ enabled: true,
341
+ teamId: input.teamId,
342
+ teamName: requestedName,
343
+ workspacePath,
344
+ revision: renamed.state.revision,
345
+ memberCount: renamed.state.members.length
346
+ }
347
+ };
348
+ };
349
+ const queue = createCommandQueue({
350
+ workspacePath,
351
+ teamId: input.teamId,
352
+ deliver: async (record) => {
353
+ if (disposing) throw new LiveTeamsError(ERROR_CODES.PLUGIN_DISPOSING, "the plugin is disposing");
354
+ return bridge.deliver(record.sessionId, {
355
+ requestId: record.requestId,
356
+ mode: record.mode,
357
+ content: record.content
358
+ });
359
+ },
360
+ bindingGenerationOf: async (sessionId) => (await storeFacade.memberBySession(input.teamId, sessionId))?.bindingGeneration
361
+ });
362
+ const mailbox = createMailbox({
363
+ workspacePath,
364
+ teamId: input.teamId,
365
+ queue,
366
+ authorize: async ({ from, to }) => {
367
+ const state = await ensureStore().then((current) => current.open());
368
+ if (state.members.some((member) => member.memberId === from)) {} else if (from === "human" || from === "scheduler") return;
369
+ const sender = state.members.find((member) => member.memberId === from);
370
+ if (sender === void 0) throw new LiveTeamsError(ERROR_CODES.CAPABILITY_UNAVAILABLE, "calling session is not bound to an active team member");
371
+ const leadId = resolveLeadMemberId(state);
372
+ const derived = (await taskStore.listTasks()).some((task) => routeAllowsContact(task, sender.memberId, to, state));
373
+ if (to === sender.memberId || to === leadId || sender.memberId === leadId || state.contactGrants?.some((grant) => grant.from === sender.memberId && grant.to === to) === true || to !== "team" && derived) return;
374
+ const target = to === "human" || to === "team" ? to : state.members.find((member) => member.memberId === to)?.displayName ?? to;
375
+ if (leadId === void 0) throw new LiveTeamsError(ERROR_CODES.CAPABILITY_UNAVAILABLE, `Not permitted to address ${target}: this team has no lead. Ask the human to set one in the Team tab.`);
376
+ throw new LiveTeamsError(ERROR_CODES.CAPABILITY_UNAVAILABLE, `Not permitted to address ${target}: peer contact is granted by your lead.\nSend this to your lead instead — the lead holds the project context and decides\nwhether a conversation with ${target} is warranted.`);
377
+ },
378
+ listRecipients: async () => {
379
+ return (await ensureStore()).open().then((team) => team.members.map((member) => ({
380
+ memberId: member.memberId,
381
+ displayName: member.displayName,
382
+ ...member.sessionId === void 0 ? {} : { sessionId: member.sessionId },
383
+ bindingGeneration: member.bindingGeneration,
384
+ active: true
385
+ })));
386
+ },
387
+ resolveSessionSender: async (sessionId) => (await storeFacade.memberBySession(input.teamId, sessionId))?.memberId,
388
+ appendAudit: async (event) => {
389
+ await audit(event);
390
+ }
391
+ });
392
+ escalationMailbox = mailbox;
393
+ const preferredKindsOf = (memberId) => {
394
+ try {
395
+ const member = readTeamStateSync({
396
+ workspacePath,
397
+ teamId: input.teamId
398
+ })?.members.find((entry) => entry.memberId === memberId);
399
+ if (member === void 0) return [];
400
+ const registry = readRoleRegistrySync(globalRolesDirectory, workspaceRolesDirectory);
401
+ return (Array.isArray(member.roles) && member.roles.length > 0 ? member.roles : [member.role]).flatMap((role) => registry.resolve(role)?.preferredTaskKinds ?? []);
402
+ } catch {
403
+ return [];
404
+ }
405
+ };
406
+ /**
407
+ * Did a human write into this Session after `since`?
408
+ *
409
+ * `source.kind === 'user'` is necessary but not sufficient: the session bridge
410
+ * delivers the plugin's own traffic through the same controller API, so a
411
+ * scheduler assignment also arrives as a user-role message (the B31 host check
412
+ * proved it on its first run). What distinguishes a human is that the plugin did
413
+ * not submit it — every command we send is recorded with its own `requestId`, and
414
+ * the delivered message carries that id as `source.rpcId`. An unloaded Session
415
+ * cannot be observed at all, and `undefined` says so rather than reporting that
416
+ * nobody was there.
417
+ */
418
+ const humanActivity = async (sessionId, since) => {
419
+ const session = ctx.sessions?.get?.(sessionId);
420
+ if (session?.snapshotEvents === void 0 || typeof session.seq !== "number") return void 0;
421
+ const mine = new Set((await queue.list()).filter((record) => record.sessionId === sessionId).map((record) => record.requestId));
422
+ return session.snapshotEvents(0, session.seq).some((event) => {
423
+ const record = event;
424
+ if (record?.type !== "user/message" || record.data?.source?.kind !== "user") return false;
425
+ if (typeof record.time !== "number" || record.time <= since) return false;
426
+ const rpcId = record.data.source.rpcId;
427
+ return !(typeof rpcId === "string" && mine.has(rpcId));
428
+ });
429
+ };
430
+ let explainedInactive = false;
431
+ dispatcher = createDispatcher({
432
+ workspacePath,
433
+ teamId: input.teamId,
434
+ readTeam: async () => (await ensureStore()).open(),
435
+ taskStore,
436
+ mailbox,
437
+ appendAudit: async (event) => {
438
+ await audit(event);
439
+ },
440
+ codingDispatch,
441
+ humanActivity,
442
+ ...sharedPaths.length === 0 ? {} : { sharedPaths },
443
+ gitTip: async (ref) => await refTip(workspacePath, ref),
444
+ enabled: enabledSync,
445
+ inactiveReason: () => {
446
+ const hint = `team "${input.teamId}" has no active state at ${teamStateFile}`;
447
+ if (!explainedInactive) {
448
+ explainedInactive = true;
449
+ ctx.logger?.warn?.(`live-teams: dispatch is idle — ${hint}`);
450
+ }
451
+ return `the team is not active: ${hint}`;
452
+ },
453
+ preferredKindsOf,
454
+ logger: (message) => ctx.logger?.warn?.(message),
455
+ ...config.dispatchSweepMs === void 0 ? {} : { sweepIntervalMs: config.dispatchSweepMs }
456
+ });
457
+ adoption = store.adoptByPath({
458
+ workspacePath,
459
+ workspaceId: input.id,
460
+ teamId: input.teamId,
461
+ actor: "plugin"
462
+ }).then((adopted) => {
463
+ if (adopted !== void 0) ctx.logger?.warn?.(`live-teams: adopted team ${adopted.adoptedFrom} for ${workspacePath} (${adopted.memberCount} member(s)) under workspace ${input.id}`);
464
+ return adopted;
465
+ }).catch((error) => {
466
+ ctx.logger?.warn?.(`live-teams: could not adopt a team for ${workspacePath}: ${error instanceof Error ? error.message : String(error)}`);
467
+ });
468
+ adoption.then(() => {
469
+ dispatcher?.start();
470
+ });
471
+ /** The projection the panel, the lead's tool and the journal all read from. */
472
+ const readDispatchProjection = async () => {
473
+ const state = await (await ensureStore()).open();
474
+ const universe = await taskStore.listTasks({ includeArchived: true });
475
+ const tasks = universe.filter((task) => task.archivedPath === void 0);
476
+ const attempts = (await taskStore.readAttempts()).attempts;
477
+ const projection = projectDispatch({
478
+ tasks,
479
+ universe,
480
+ attempts,
481
+ team: state,
482
+ memberOf: (memberId) => state.members.find((member) => member.memberId === memberId),
483
+ preferredKindsOf,
484
+ dispatch: {
485
+ ...readDispatchSwitch(state),
486
+ coding: codingDispatch
487
+ }
488
+ });
489
+ return {
490
+ teamId: input.teamId,
491
+ paused: projection.paused,
492
+ coding: projection.coding,
493
+ ...projection.pausedBy === void 0 ? {} : { pausedBy: projection.pausedBy },
494
+ ...projection.pausedReason === void 0 ? {} : { pausedReason: projection.pausedReason },
495
+ counts: projection.counts,
496
+ tasks: projection.evaluations
497
+ };
498
+ };
499
+ /** One switch, two callers: the desktop route admits the human, the tool admits the lead. */
500
+ const setDispatchMode = async (mode, actor, reason) => {
501
+ const store_ = await ensureStore();
502
+ const current = await store_.open();
503
+ if (actor !== "human:desktop" && actor !== "local:unverified") {
504
+ const leadId = current.leadMemberId ?? (current.members.filter((member) => member.role === "lead").length === 1 ? current.members.find((member) => member.role === "lead")?.memberId : void 0);
505
+ if (leadId === void 0 || actor !== leadId) throw new LiveTeamsError(ERROR_CODES.CAPABILITY_UNAVAILABLE, "only the human or the lead may change dispatch");
506
+ }
507
+ const result = await store_.setDispatchMode(input.teamId, mode, {
508
+ actor,
509
+ ...reason === void 0 ? {} : { reason }
510
+ }, current.revision);
511
+ await audit({
512
+ kind: mode === "paused" ? "dispatch/paused" : "dispatch/resumed",
513
+ teamId: input.teamId,
514
+ from: result.from,
515
+ to: result.to,
516
+ ...reason === void 0 ? {} : { reason },
517
+ actor,
518
+ at: Date.now()
519
+ });
520
+ return {
521
+ from: result.from,
522
+ to: result.to
523
+ };
524
+ };
525
+ const copyRoleOverride = async (id) => {
526
+ const registry = roleRegistry ?? await readRoles();
527
+ const globalPath = registry.globalRolePath(id);
528
+ if (globalPath === void 0 || !registry.globalRoleIds.has(id)) throw new LiveTeamsError(ERROR_CODES.CAPABILITY_UNAVAILABLE, "unknown global role");
529
+ const target = path.join(workspaceRolesDirectory, `${id}.md`);
530
+ try {
531
+ await access(target);
532
+ throw new LiveTeamsError(ERROR_CODES.CAPABILITY_UNAVAILABLE, "role already overridden", { auditDetail: "role already overridden" });
533
+ } catch (error) {
534
+ if (error instanceof LiveTeamsError) throw error;
535
+ }
536
+ await mkdir(workspaceRolesDirectory, { recursive: true });
537
+ try {
538
+ await copyFile(globalPath, target, 0);
539
+ } catch (error) {
540
+ throw new Error(error instanceof Error ? error.message : String(error));
541
+ }
542
+ roleRegistry = void 0;
543
+ return {
544
+ id,
545
+ path: target
546
+ };
547
+ };
548
+ const routeOptions = {
549
+ teamId: input.teamId,
550
+ compatibility: () => compatibility,
551
+ setLead: async (memberId, expectedRevision) => {
552
+ return {
553
+ state: await store.setLead(input.teamId, memberId, expectedRevision),
554
+ leadMemberId: memberId
555
+ };
556
+ },
557
+ setContactGrant: async (from, to, action, actor) => {
558
+ const result = await store.updateContactGrant(input.teamId, from, to, action, actor);
559
+ if (result.changed) await store.appendEvent(input.teamId, {
560
+ kind: action === "add" ? "contact/granted" : "contact/revoked",
561
+ from,
562
+ to,
563
+ actor,
564
+ at: Date.now()
565
+ });
566
+ return { grants: result.grants };
567
+ },
568
+ teamName: input.teamName,
569
+ workspacePath,
570
+ titleSession: async (request) => titleSession(request.sessionId, request.displayName, request.reason, request.previousTitle),
571
+ ...ctx.logger === void 0 ? {} : { logger: ctx.logger },
572
+ readRoles,
573
+ teamStatus: status,
574
+ teamEnabled: async () => {
575
+ try {
576
+ return enabledSync();
577
+ } catch {
578
+ return await store.readExisting() !== void 0 && !await store.isDisabled(input.teamId);
579
+ }
580
+ },
581
+ enableTeam,
582
+ disableTeam,
583
+ renameTeam,
584
+ listTeamCandidates,
585
+ adoptTeam,
586
+ canOpenWorkspacePath: async () => typeof ctx.sessionController.canOpenWorkspacePath === "function" && await ctx.sessionController.canOpenWorkspacePath(),
587
+ openWorkspacePath: async (request, signal) => {
588
+ if (typeof ctx.sessionController.openWorkspacePath !== "function") throw new Error("session path opener unavailable");
589
+ return ctx.sessionController.openWorkspacePath(request, signal);
590
+ },
591
+ copyRoleOverride,
592
+ mailbox,
593
+ taskStore,
594
+ workspaceRegistered: () => entryForPath(workspacePath) !== void 0,
595
+ ...ctx.agents?.get === void 0 ? {} : { agents: { get: ctx.agents.get.bind(ctx.agents) } },
596
+ listCandidates: async () => {
597
+ const controller = ctx.sessionController;
598
+ if (controller === void 0 || controller === null || typeof controller.list !== "function") return [];
599
+ const listed = await controller.list({}, new AbortController().signal);
600
+ const value = listed !== null && typeof listed === "object" && !Array.isArray(listed) ? listed : {};
601
+ if (!Array.isArray(value.items)) return [];
602
+ const descriptors = [];
603
+ for (const raw of value.items) {
604
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) continue;
605
+ const item = raw;
606
+ const sessionId = item.sessionId;
607
+ const cwd = item.cwd;
608
+ if (typeof sessionId !== "string" || sessionId.length === 0 || typeof cwd !== "string" || cwd.length === 0) continue;
609
+ if (item.origin === "subagent") continue;
610
+ const liveAgent = ctx.agents?.get?.(sessionId);
611
+ const parentSessionId = typeof item.parentSessionId === "string" ? item.parentSessionId : void 0;
612
+ const parentAgent = parentSessionId === void 0 ? void 0 : ctx.agents?.get?.(parentSessionId);
613
+ if (parentSessionId !== void 0 && liveAgent !== void 0 && liveAgent !== null && parentAgent !== void 0 && parentAgent !== null && ctx.agents?.isOwnedBy?.(sessionId, parentAgent) === true) continue;
614
+ const runtime = liveAgent !== void 0 && liveAgent !== null ? liveAgent : void 0;
615
+ const titleValue = (item.projections !== null && typeof item.projections === "object" && !Array.isArray(item.projections) ? item.projections : void 0)?.values?.title;
616
+ descriptors.push({
617
+ sessionId,
618
+ cwd,
619
+ title: typeof titleValue === "string" ? titleValue : void 0,
620
+ live: liveAgent !== void 0 && liveAgent !== null,
621
+ status: runtime?.status === "running" ? "running" : runtime?.status === "idle" ? "idle" : "unknown",
622
+ subagentOwned: false,
623
+ workspaceId: void 0,
624
+ seq: 0,
625
+ agentPreset: void 0,
626
+ blank: item.blank === true
627
+ });
628
+ }
629
+ return descriptors;
630
+ },
631
+ createMember: async (request) => {
632
+ const descriptor = await assertSessionInTeamWorkspace({
633
+ bridge,
634
+ workspacePath
635
+ }, request.sessionId);
636
+ const current = await ensureStore();
637
+ const memberId = request.memberId ?? `m-${randomUUID()}`;
638
+ const result = await current.commit(input.teamId, {
639
+ expectedRevision: request.expectedRevision,
640
+ mutate: (state) => {
641
+ if (state.members.some((entry) => entry.memberId === memberId)) throw new LiveTeamsError(ERROR_CODES.CAPABILITY_UNAVAILABLE, "member id is already taken");
642
+ if (state.members.some((entry) => entry.sessionId === request.sessionId)) throw new LiveTeamsError(ERROR_CODES.CAPABILITY_UNAVAILABLE, "session is already bound");
643
+ const member = {
644
+ memberId,
645
+ displayName: request.displayName,
646
+ role: request.role,
647
+ availability: "available",
648
+ sessionId: request.sessionId,
649
+ bindingGeneration: 1,
650
+ contractRevision: 0
651
+ };
652
+ state.members.push(member);
653
+ return member;
654
+ }
655
+ });
656
+ sessionBindings.set(request.sessionId, binding);
657
+ const sessionTitle = await titleSession(descriptor.sessionId, result.value.displayName, "member-added", descriptor.title);
658
+ return {
659
+ state: result.state,
660
+ member: result.value,
661
+ ...sessionTitle === void 0 ? {} : { sessionTitle }
662
+ };
663
+ },
664
+ createSessionMember: async (request) => {
665
+ const workspace = entryForPath(workspacePath);
666
+ if (workspace === void 0 || typeof workspace.id !== "string" || workspace.id.length === 0) throw new LiveTeamsError(ERROR_CODES.SESSION_WRONG_WORKSPACE, "workspace not registered", { auditDetail: "workspace not registered" });
667
+ const descriptor = await bridge.create({
668
+ sessionId: request.sessionId,
669
+ workspaceId: workspace.id
670
+ });
671
+ assertSessionDescriptorInTeamWorkspace({ workspacePath }, descriptor);
672
+ const current = await ensureStore();
673
+ const memberId = `m-${randomUUID()}`;
674
+ const commitMember = async () => await current.commit(input.teamId, {
675
+ expectedRevision: request.expectedRevision,
676
+ mutate: (state) => {
677
+ if (state.members.some((entry) => entry.sessionId === descriptor.sessionId)) throw new LiveTeamsError(ERROR_CODES.CAPABILITY_UNAVAILABLE, "session is already bound");
678
+ const member = {
679
+ memberId,
680
+ displayName: request.displayName,
681
+ role: request.role,
682
+ availability: "available",
683
+ sessionId: descriptor.sessionId,
684
+ bindingGeneration: 1,
685
+ contractRevision: 0
686
+ };
687
+ state.members.push(member);
688
+ return member;
689
+ }
690
+ });
691
+ const result = await commitMember().catch(async (error) => {
692
+ await audit({
693
+ kind: "session/orphaned",
694
+ teamId: input.teamId,
695
+ sessionId: descriptor.sessionId,
696
+ reason: error instanceof Error ? error.message : String(error),
697
+ actor: "plugin",
698
+ at: Date.now()
699
+ });
700
+ throw error;
701
+ });
702
+ sessionBindings.set(descriptor.sessionId, binding);
703
+ const sessionTitle = await titleSession(descriptor.sessionId, result.value.displayName, "member-added", descriptor.title);
704
+ return {
705
+ state: result.state,
706
+ member: result.value,
707
+ ...sessionTitle === void 0 ? {} : { sessionTitle }
708
+ };
709
+ },
710
+ removeMember: async (request) => {
711
+ const held = (await taskStore.readAttempts()).attempts.find((attempt) => attempt.status === "active" && attempt.memberId === request.memberId);
712
+ if (held !== void 0) {
713
+ const task = (await taskStore.listTasks({ includeArchived: true })).find((entry) => entry.id === held.taskId);
714
+ throw new LiveTeamsError(ERROR_CODES.CAPABILITY_UNAVAILABLE, `${request.memberId} holds an active attempt on ${held.taskId}${task === void 0 ? "" : ` "${task.title}"`}; release the attempt before removing the member`, { auditDetail: "member holds an active attempt" });
715
+ }
716
+ return await (await ensureStore()).removeMember(input.teamId, request.memberId, request.expectedRevision);
717
+ },
718
+ applyMutation: async (request) => {
719
+ const current = await ensureStore();
720
+ const prior = (await current.open()).members.find((entry) => entry.memberId === request.memberId);
721
+ let priorDescriptor;
722
+ if (request.kind === "name" && prior?.sessionId !== void 0) try {
723
+ priorDescriptor = await bridge.inspect(prior.sessionId);
724
+ } catch (error) {
725
+ ctx.logger?.warn?.(`live-teams: could not inspect Session "${prior.sessionId}" before title sync: ${String(error)}`);
726
+ }
727
+ const result = await current.commit(input.teamId, {
728
+ expectedRevision: request.expectedRevision,
729
+ mutate: (state) => {
730
+ const member = state.members.find((entry) => entry.memberId === request.memberId);
731
+ if (member === void 0) throw new LiveTeamsError(ERROR_CODES.CAPABILITY_UNAVAILABLE, "member not found");
732
+ const from = request.kind === "role" ? member.role : request.kind === "availability" ? member.availability : member.displayName;
733
+ if (request.kind === "role") member.role = request.value;
734
+ else if (request.kind === "availability") member.availability = request.value;
735
+ else member.displayName = request.value;
736
+ return {
737
+ member,
738
+ from,
739
+ to: request.value
740
+ };
741
+ }
742
+ });
743
+ const sessionTitle = request.kind === "name" && priorDescriptor !== void 0 ? await titleSession(priorDescriptor.sessionId, result.value.member.displayName, "member-renamed", priorDescriptor.title) : void 0;
744
+ return {
745
+ state: result.state,
746
+ member: result.value.member,
747
+ from: result.value.from,
748
+ to: result.value.to,
749
+ ...sessionTitle === void 0 ? {} : { sessionTitle }
750
+ };
751
+ },
752
+ appendAudit: async (event) => {
753
+ await audit(event);
754
+ },
755
+ readDispatch: readDispatchProjection,
756
+ setDispatch: async (request) => await setDispatchMode(request.mode, request.actor, request.reason),
757
+ readEvents: async (after) => await (await ensureStore()).readEvents(input.teamId, after),
758
+ interrupt: (sessionId, reason) => bridge.interrupt(sessionId, reason)
759
+ };
760
+ const contextProvider = createMembershipContextProvider({
761
+ store: storeFacade,
762
+ teamId: input.teamId,
763
+ teamName: teamNameSync,
764
+ workspacePath,
765
+ enabled: enabledSync,
766
+ roleRegistry: { resolve: resolveRoleSync },
767
+ formalToolsAvailable: toolRuntime !== void 0,
768
+ live: (member) => member.sessionId !== void 0 && ctx.agents?.get?.(member.sessionId) !== void 0,
769
+ commentsSync: (_teamId, memberId) => mailbox.unreadCommentsForSync(memberId),
770
+ onError: (error) => ctx.logger?.warn?.(`live-teams: membership context unavailable: ${String(error)}`)
771
+ });
772
+ const binding = {
773
+ path: workspacePath,
774
+ id: input.id,
775
+ teamId: input.teamId,
776
+ teamName: input.teamName,
777
+ store,
778
+ taskStore,
779
+ storeFacade,
780
+ contextBinding,
781
+ contextProvider,
782
+ routeOptions,
783
+ queue,
784
+ mailbox,
785
+ dispatcher,
786
+ adoption,
787
+ readDispatch: readDispatchProjection,
788
+ setDispatch: setDispatchMode,
789
+ ensureStore
790
+ };
791
+ bindingsByWorkspaceId.set(input.id, binding);
792
+ return binding;
793
+ }
794
+ const pinnedEntry = configuredPath === void 0 ? void 0 : entryForPath(configuredPath);
795
+ const pinnedWorkspaceId = configuredPath === void 0 ? void 0 : config.workspaceId ?? pinnedEntry?.id;
796
+ const pinnedTeamId = config.teamId ?? config.workspaceId ?? (configuredPath === void 0 ? "" : path.basename(configuredPath));
797
+ const pinnedTeamName = config.teamName === void 0 || config.teamName.length === 0 ? pinnedTeamId : config.teamName;
798
+ const toolRuntime = ctx.get?.("tools");
799
+ const pinnedBinding = configuredPath === void 0 ? void 0 : bindingFor({
800
+ path: configuredPath,
801
+ id: pinnedWorkspaceId ?? pinnedTeamId,
802
+ teamId: pinnedTeamId,
803
+ teamName: pinnedTeamName
804
+ });
805
+ for (const entry of registryEntries()) {
806
+ if (typeof entry.path !== "string" || entry.path.length === 0 || typeof entry.id !== "string" || entry.id.length === 0) continue;
807
+ const entryPath = path.resolve(entry.path);
808
+ const entryTeamName = configuredPath !== void 0 && canonicalize(entryPath) === canonicalize(configuredPath) ? pinnedTeamName : workspaceTitle(entry, entryPath, entry.id);
809
+ const binding = configuredPath !== void 0 && canonicalize(entryPath) === canonicalize(configuredPath) ? pinnedBinding : bindingFor({
810
+ path: entryPath,
811
+ id: entry.id,
812
+ teamId: entry.id,
813
+ teamName: entryTeamName
814
+ });
815
+ if (binding === void 0) continue;
816
+ for (const sessionId of entry.sessionIds ?? []) if (typeof sessionId === "string" && sessionId.length > 0) sessionBindings.set(sessionId, binding);
817
+ }
818
+ const dynamicContextProvider = createMembershipContextProvider({
819
+ ...pinnedBinding === void 0 ? {} : {
820
+ store: pinnedBinding.storeFacade,
821
+ teamId: pinnedBinding.teamId,
822
+ teamName: pinnedBinding.teamName,
823
+ enabled: () => enabledSyncForBinding(pinnedBinding)
824
+ },
825
+ ...pinnedBinding === void 0 ? { resolveSession: (sessionId) => sessionBindings.get(sessionId)?.contextBinding } : {},
826
+ formalToolsAvailable: toolRuntime !== void 0,
827
+ onError: (error) => ctx.logger?.warn?.(`live-teams: membership context unavailable: ${String(error)}`)
828
+ });
829
+ function enabledSyncForBinding(binding) {
830
+ try {
831
+ return binding.contextBinding.enabled?.() !== false;
832
+ } catch {
833
+ return false;
834
+ }
835
+ }
836
+ const contextProvider = pinnedBinding === void 0 ? dynamicContextProvider : pinnedBinding.contextProvider;
837
+ ctx.systemPrompt.context(contextProvider.definition());
838
+ registerMemberToolsIfAvailable(toolRuntime, async (sessionId) => {
839
+ const binding = await bindingForSession(sessionId);
840
+ if (binding === void 0 || binding.contextBinding.enabled?.() === false) return void 0;
841
+ const member = await binding.storeFacade.memberBySession(binding.teamId, sessionId);
842
+ if (member === void 0 || member.sessionId !== sessionId) return void 0;
843
+ const team = await binding.ensureStore().then((store) => store.open());
844
+ return {
845
+ mailbox: binding.mailbox,
846
+ teamId: binding.teamId,
847
+ teamName: team.name ?? binding.teamName,
848
+ member,
849
+ team,
850
+ workspacePath: binding.path,
851
+ taskStore: binding.taskStore,
852
+ setMemberNote: async (memberId, note) => {
853
+ const current = await binding.ensureStore();
854
+ const state = await current.open();
855
+ const result = await current.setMemberNote(binding.teamId, memberId, note, state.revision);
856
+ return {
857
+ memberId: result.member.memberId,
858
+ note: result.member.note ?? ""
859
+ };
860
+ },
861
+ setDispatch: async (request) => await binding.setDispatch(request.mode, request.actor, request.reason),
862
+ updateContactGrant: async (from, to, action, actor) => {
863
+ const current = await binding.ensureStore();
864
+ const state = await current.open();
865
+ const target = state.members.some((entry) => entry.memberId === to) || to === "human" || to === "team";
866
+ if (!state.members.some((entry) => entry.memberId === from) || !target) throw new LiveTeamsError(ERROR_CODES.CAPABILITY_UNAVAILABLE, "memberId and allow must name members of this team, human, or team");
867
+ const result = await current.updateContactGrant(binding.teamId, from, to, action, actor);
868
+ if (result.changed) await current.appendEvent(binding.teamId, {
869
+ kind: action === "add" ? "contact/granted" : "contact/revoked",
870
+ from,
871
+ to,
872
+ actor,
873
+ at: Date.now()
874
+ });
875
+ return { grants: result.grants };
876
+ },
877
+ appendAudit: async (event) => (await binding.ensureStore()).appendEvent(binding.teamId, event),
878
+ live: (candidate) => candidate.sessionId !== void 0 && binding.storeFacade.memberBySessionSync(binding.teamId, candidate.sessionId)?.memberId === candidate.memberId
879
+ };
880
+ }, {
881
+ includeContext: true,
882
+ includeContact: true,
883
+ includeTasks: true,
884
+ includeSubmissions: true,
885
+ includeReview: true,
886
+ includeAcceptance: true,
887
+ includeDispatch: true
888
+ });
889
+ const rootOptions = pinnedBinding?.routeOptions ?? {
890
+ teamId: "workspace-scoped",
891
+ teamName: "workspace-scoped",
892
+ workspacePath: "",
893
+ resolveRequest: async (_request, sessionId) => {
894
+ return (await bindingForSession(sessionId))?.routeOptions;
895
+ }
896
+ };
897
+ /**
898
+ * Resolve the binding that owns a Session, caching the answer.
899
+ *
900
+ * The cache is primed from the workspace registry at mount, which misses a
901
+ * Session created afterwards — and in a pinned deployment nothing else
902
+ * refreshes it. A miss therefore resolves directly from the Session, and the
903
+ * pinned workspace answers for its own directory, so a member added at any
904
+ * time can still act.
905
+ */
906
+ async function bindingForSession(sessionId) {
907
+ if (sessionId === void 0 || sessionId.length === 0) return void 0;
908
+ const cached = sessionBindings.get(sessionId);
909
+ if (cached !== void 0) return cached;
910
+ const descriptor = await bridge.inspect(sessionId);
911
+ if (descriptor === void 0 || descriptor.sessionId !== sessionId || descriptor.cwd === void 0 || descriptor.cwd.length === 0) return void 0;
912
+ const entry = entryForPath(descriptor.cwd);
913
+ if (entry !== void 0 && entry.id.length > 0) {
914
+ const entryPath = path.resolve(entry.path);
915
+ const binding = bindingFor({
916
+ path: entryPath,
917
+ id: entry.id,
918
+ teamId: entry.id,
919
+ teamName: workspaceTitle(entry, entryPath, entry.id)
920
+ });
921
+ sessionBindings.set(sessionId, binding);
922
+ return binding;
923
+ }
924
+ if (pinnedBinding !== void 0 && canonicalize(descriptor.cwd) === canonicalize(configuredPath)) {
925
+ sessionBindings.set(sessionId, pinnedBinding);
926
+ return pinnedBinding;
927
+ }
928
+ }
929
+ (pinnedBinding === void 0 ? Promise.resolve(void 0) : pinnedBinding.store.readExisting()).catch((error) => {
930
+ ctx.logger?.warn?.(`live-teams: store unavailable: ${String(error)}`);
931
+ });
932
+ const initialReadRoles = pinnedBinding?.routeOptions.readRoles;
933
+ if (initialReadRoles !== void 0) initialReadRoles().catch((error) => {
934
+ ctx.logger?.warn?.(`live-teams: role registry unavailable: ${String(error)}`);
935
+ });
936
+ if (ctx.inject !== void 0) ctx.inject(["webServer"], (webCtx) => {
937
+ webCtx.effect(() => registerLiveTeamsRoutes(webCtx, rootOptions), "live-teams-host: routes");
938
+ });
939
+ else if (ctx.webServer !== void 0) ctx.effect(() => registerLiveTeamsRoutes(ctx, rootOptions), "live-teams-host: routes");
940
+ const defaultBinding = pinnedBinding ?? [...bindingsByWorkspaceId.values()][0];
941
+ const service = Object.freeze({
942
+ teamId: defaultBinding?.teamId ?? "",
943
+ workspacePath: defaultBinding?.path ?? "",
944
+ bridge,
945
+ queue: defaultBinding?.queue ?? createCommandQueue({
946
+ workspacePath: process.cwd(),
947
+ teamId: "workspace-scoped",
948
+ deliver: async () => {
949
+ throw new LiveTeamsError(ERROR_CODES.CAPABILITY_UNAVAILABLE, "workspace-scoped queue requires a resolved workspace");
950
+ }
951
+ }),
952
+ mailbox: defaultBinding?.mailbox ?? createMailbox({
953
+ workspacePath: process.cwd(),
954
+ teamId: "workspace-scoped",
955
+ queue: createCommandQueue({
956
+ workspacePath: process.cwd(),
957
+ teamId: "workspace-scoped",
958
+ deliver: async () => {
959
+ throw new LiveTeamsError(ERROR_CODES.CAPABILITY_UNAVAILABLE, "workspace-scoped mailbox requires a resolved workspace");
960
+ }
961
+ }),
962
+ listRecipients: async () => []
963
+ }),
964
+ contextProvider,
965
+ store: async () => {
966
+ if (defaultBinding === void 0) throw new LiveTeamsError(ERROR_CODES.SESSION_WRONG_WORKSPACE, "no workspace is resolved");
967
+ return defaultBinding.ensureStore();
968
+ },
969
+ taskStore: async () => {
970
+ if (defaultBinding === void 0) throw new LiveTeamsError(ERROR_CODES.SESSION_WRONG_WORKSPACE, "no workspace is resolved");
971
+ return defaultBinding.taskStore;
972
+ },
973
+ readDispatch: async () => {
974
+ if (defaultBinding === void 0) throw new LiveTeamsError(ERROR_CODES.SESSION_WRONG_WORKSPACE, "no workspace is resolved");
975
+ return await defaultBinding.readDispatch();
976
+ },
977
+ reconcile: async (trigger = "manual") => {
978
+ if (defaultBinding === void 0) throw new LiveTeamsError(ERROR_CODES.SESSION_WRONG_WORKSPACE, "no workspace is resolved");
979
+ return await defaultBinding.dispatcher.reconcile(trigger);
980
+ },
981
+ openMember: async (sessionId) => {
982
+ const binding = sessionBindings.get(sessionId);
983
+ return binding === void 0 ? void 0 : binding.storeFacade.memberBySession(binding.teamId, sessionId);
984
+ },
985
+ bindMember: async (request) => {
986
+ const binding = sessionBindings.get(request.sessionId);
987
+ if (binding === void 0) throw new LiveTeamsError(ERROR_CODES.SESSION_WRONG_WORKSPACE, "no workspace is resolved");
988
+ await assertSessionInTeamWorkspace({
989
+ bridge,
990
+ workspacePath: binding.path
991
+ }, request.sessionId);
992
+ return (await binding.ensureStore()).bindSession(binding.teamId, request.memberId, request.sessionId, request.expectedRevision);
993
+ },
994
+ /** What the plugin found when it asked the host what it offers. */
995
+ compatibility,
996
+ get disposing() {
997
+ return disposing;
998
+ }
999
+ });
1000
+ ctx.provide("liveTeams", service);
1001
+ }
1002
+ //#endregion
1003
+ export { apply, apply as default, inject, name };