@khorsheed/dsh-room 0.1.1 → 0.2.1

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 (40) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.en.md +9 -8
  3. package/README.i18n.yaml +2 -2
  4. package/README.md +9 -8
  5. package/lib/client.js +3422 -1481
  6. package/lib/index.js +88 -12
  7. package/lib/{journal-CuRnWk66.js → journal-BWL3-i0s.js} +27 -0
  8. package/lib/tool.js +63 -11
  9. package/lib/typert.host.js +161 -26
  10. package/lib/typert.remote-client.d.ts +2 -0
  11. package/lib/typert.remote-client.js +142 -22
  12. package/lib/types/adapter.d.ts +2 -2
  13. package/lib/types/client/RoomActivityDock.d.ts +19 -0
  14. package/lib/types/client/RoomActivityDock.js +105 -0
  15. package/lib/types/client/RoomComposer.d.ts +1 -1
  16. package/lib/types/client/RoomComposer.js +3 -3
  17. package/lib/types/client/RoomPlanTab.d.ts +15 -0
  18. package/lib/types/client/RoomPlanTab.js +13 -0
  19. package/lib/types/client/RoomPlanView.d.ts +3 -2
  20. package/lib/types/client/RoomPlanView.js +5 -6
  21. package/lib/types/client/executions.d.ts +50 -0
  22. package/lib/types/client/executions.js +70 -0
  23. package/lib/types/client/index.d.ts +1 -1
  24. package/lib/types/client/index.js +33 -1
  25. package/lib/types/client/locales.d.ts +50 -0
  26. package/lib/types/client/locales.js +50 -0
  27. package/lib/types/client/preset-visibility.d.ts +3 -3
  28. package/lib/types/client/preset-visibility.js +2 -2
  29. package/lib/types/client/slots.d.ts +6 -1
  30. package/lib/types/dispatch.js +30 -6
  31. package/lib/types/index.d.ts +5 -0
  32. package/lib/types/index.js +42 -7
  33. package/lib/types/journal.d.ts +1 -1
  34. package/lib/types/journal.js +27 -1
  35. package/lib/types/tool-definition.d.ts +56 -0
  36. package/lib/types/tool-definition.js +393 -0
  37. package/lib/types/tool.d.ts +37 -49
  38. package/lib/types/tool.js +47 -381
  39. package/lib/types/types.d.ts +28 -0
  40. package/package.json +27 -26
@@ -0,0 +1,50 @@
1
+ import type { RoomExecution, RoomState } from '../types.ts';
2
+ export interface ExecutionRow {
3
+ sessionTotal?: boolean;
4
+ id: string;
5
+ member: string;
6
+ childSessionId?: string;
7
+ title: string;
8
+ status: 'queued' | 'running' | 'done' | 'cancelled' | 'failed' | 'uncertain' | 'submitted' | 'accepted' | 'settled';
9
+ provider?: string;
10
+ model?: string;
11
+ effort?: string;
12
+ tokens?: number;
13
+ startedAt?: number;
14
+ elapsedMs?: number;
15
+ error?: string;
16
+ execution?: RoomExecution;
17
+ }
18
+ /** Public session-list projection; members are deduplicated against durable Room executions. */
19
+ export interface BackgroundSession {
20
+ id: string;
21
+ parentId?: string;
22
+ origin?: string;
23
+ displayTitle?: string;
24
+ running?: boolean;
25
+ projectionValues?: {
26
+ tokenUsage?: {
27
+ uncachedInputTokens: number;
28
+ outputTokens: number;
29
+ cacheReadTokens: number;
30
+ cacheWriteTokens: number;
31
+ };
32
+ subagentTiming?: {
33
+ settledMs: number;
34
+ active?: {
35
+ since: number;
36
+ through: number;
37
+ };
38
+ };
39
+ };
40
+ }
41
+ export interface BackgroundSessionsStore {
42
+ subscribe(listener: () => void): () => void;
43
+ getSnapshot(): {
44
+ byId: Readonly<Record<string, BackgroundSession>>;
45
+ };
46
+ }
47
+ export declare function descendantRows(sessions: Readonly<Record<string, BackgroundSession>>, root: string, state: RoomState, now: number, active?: readonly string[]): ExecutionRow[];
48
+ /** One row per delivery/attempt, not one row per persistent member session. */
49
+ export declare function executionRows(state: RoomState): ExecutionRow[];
50
+ //# sourceMappingURL=executions.d.ts.map
@@ -0,0 +1,70 @@
1
+ export function descendantRows(sessions, root, state, now, active = []) {
2
+ const known = new Set([...state.members.map(member => member.childSessionId), ...(state.executions ?? []).map(run => run.childSessionId)]);
3
+ const representedRunning = new Set(executionRows(state).filter(row => row.status === 'running').map(row => row.childSessionId));
4
+ const coordinatorChild = state.members.find(member => state.coordinator === undefined ? member.kind === 'main-agent' : member.id === state.coordinator.memberId)?.childSessionId;
5
+ const result = [];
6
+ const seen = new Set([root]);
7
+ const visit = (parent) => {
8
+ for (const child of Object.values(sessions)) {
9
+ if (child.parentId !== parent || child.origin !== 'subagent' || seen.has(child.id))
10
+ continue;
11
+ seen.add(child.id);
12
+ const running = child.running === true || active.includes(child.id);
13
+ // Direct follow-ups in a member's own composer bypass Room dispatch.
14
+ // Keep that live session visible without duplicating an active Room execution.
15
+ if (child.id !== coordinatorChild && (!known.has(child.id) || (running && !representedRunning.has(child.id)))) {
16
+ const timing = child.projectionValues?.subagentTiming;
17
+ const usage = child.projectionValues?.tokenUsage;
18
+ const elapsedMs = timing === undefined ? undefined : timing.settledMs + (timing.active === undefined ? 0 : Math.max(0, (running ? now : timing.active.through) - timing.active.since));
19
+ const tokens = usage === undefined ? undefined : usage.uncachedInputTokens + usage.outputTokens + usage.cacheReadTokens + usage.cacheWriteTokens;
20
+ result.push({ id: `session:${child.id}`, member: child.displayTitle || child.id, title: child.displayTitle || child.id, childSessionId: child.id,
21
+ status: running ? 'running' : 'settled', sessionTotal: true,
22
+ ...elapsedMs === undefined ? {} : { elapsedMs },
23
+ ...tokens === undefined || !Number.isFinite(tokens) ? {} : { tokens },
24
+ });
25
+ }
26
+ visit(child.id);
27
+ }
28
+ };
29
+ visit(root);
30
+ return result;
31
+ }
32
+ /** One row per delivery/attempt, not one row per persistent member session. */
33
+ export function executionRows(state) {
34
+ const coordinator = state.members.find(member => state.coordinator === undefined ? member.kind === 'main-agent' : member.id === state.coordinator.memberId);
35
+ const history = new Map((state.executions ?? state.runs.map(run => ({ ...run, id: run.runId ?? `${run.member}:${run.startedAt}` }))).map(run => [run.id, run]));
36
+ const rows = [];
37
+ for (const delivery of state.deliveries ?? []) {
38
+ const execution = history.get(delivery.id);
39
+ history.delete(delivery.id);
40
+ const member = state.members.find(member => member.id === delivery.memberId);
41
+ if ((coordinator !== undefined && delivery.memberId === coordinator.id) || delivery.origin === 'report')
42
+ continue;
43
+ const task = delivery.plan?.goalId === state.plan?.id ? state.plan?.tasks.find(task => task.id === delivery.plan?.taskId) : undefined;
44
+ const attempt = task?.attempts.find(attempt => attempt.id === delivery.plan?.attemptId);
45
+ const terminal = delivery.status === 'done' && attempt !== undefined
46
+ ? attempt.status === 'accepted' ? 'accepted' : attempt.status === 'submitted' ? 'submitted' : 'done'
47
+ : delivery.status;
48
+ const status = execution?.state === 'failed' && delivery.status === 'running' ? 'uncertain' : terminal;
49
+ const title = task?.title ?? state.tasks.find(task => task.deliveryId === delivery.id)?.title ?? delivery.text;
50
+ rows.push({ title: title.replace(/\s+/g, ' ').trim(),
51
+ ...member?.provider === undefined ? {} : { provider: member.provider },
52
+ ...execution,
53
+ // Execution identity is immutable; delivery/review state is authoritative.
54
+ id: delivery.id, status, member: member?.name ?? execution?.member ?? delivery.memberId,
55
+ ...execution === undefined ? {} : { execution },
56
+ ...execution?.childSessionId !== undefined || member?.childSessionId === undefined ? {} : { childSessionId: member.childSessionId },
57
+ ...delivery.error === undefined ? {} : { error: delivery.error },
58
+ });
59
+ }
60
+ for (const execution of history.values()) {
61
+ const member = state.members.find(member => member.id === execution.memberId || (execution.memberId === undefined && member.name === execution.member));
62
+ if (coordinator !== undefined && member === coordinator)
63
+ continue;
64
+ rows.push({ ...execution, execution, title: state.tasks.find(task => task.deliveryId === execution.runId)?.title ?? execution.member,
65
+ status: execution.state, member: member?.name ?? execution.member,
66
+ ...execution.childSessionId !== undefined || member?.childSessionId === undefined ? {} : { childSessionId: member.childSessionId },
67
+ });
68
+ }
69
+ return rows.reverse();
70
+ }
@@ -4,7 +4,7 @@
4
4
  * registers the slot entries: the session-header「邀请 agent」action and the
5
5
  * 成员 `conversation.view` tab — both self-hiding by the current session's
6
6
  * preset composition (M3': visible exactly when the composition names the
7
- * `@khorsheed/dsh-room-tool` row, fail-open on every unreadable path, and
7
+ * `@khorsheed/dsh-room/tool` row, fail-open on every unreadable path, and
8
8
  * always visible inside an actual room) — plus the `conversation.composer`
9
9
  * chain takeover (claims the composer exactly when the current session is a
10
10
  * cached room — and renders the dock capsules plus the session stats row
@@ -4,6 +4,7 @@ import { en, zh } from './locales.js';
4
4
  import { InviteAgentAction } from './InviteAgentAction.js';
5
5
  import { MembersView } from './MembersView.js';
6
6
  import { RoomComposer, selectRoomComposer } from './RoomComposer.js';
7
+ import { ROOM_PLAN_KIND, ROOM_PLAN_TAB_ID, RoomPlanTab } from './RoomPlanTab.js';
7
8
  import { RoomSpeechView } from './RoomSpeechView.js';
8
9
  import { RoomRunView } from './RoomRunView.js';
9
10
  import { RoomEventView } from './RoomEventView.js';
@@ -360,6 +361,8 @@ export async function apply(ctx) {
360
361
  // main-agent turn Stop: the takeover renders the dock
361
362
  // surfaces and the Stop button itself, because their seats hide with the
362
363
  // official fallback.
364
+ const sidebar = ctx.get('sidebarRight');
365
+ const sidebarTabs = ctx.get('sidebarRightTabs');
363
366
  const composerEntry = {
364
367
  name: 'conversation.composer',
365
368
  priority: -10,
@@ -379,10 +382,26 @@ export async function apply(ctx) {
379
382
  return result?.ok && result.value.ok ? { ok: true } : { ok: false, message: result?.ok && !result.value.ok ? result.value.message : t('composer.error.generic') };
380
383
  },
381
384
  openPlanSession: id => openSession(id),
385
+ backgroundSessions: ctx.sessions.list,
386
+ activeChildren: async () => {
387
+ const gateway = ctx.get('remote.localAgentGateway');
388
+ const result = await gateway?.activeDelegations?.();
389
+ return result?.ok ? result.value : undefined;
390
+ },
391
+ stopChild: async (childSessionId) => {
392
+ const result = await remote?.cancelChild({ sessionId, childSessionId: childSessionId });
393
+ return result?.ok && result.value.ok && result.value.value.cancelled ? { ok: true } : { ok: false, message: t('activity.stopChanged') };
394
+ },
395
+ ...sidebar === undefined || sidebarTabs === undefined ? {} : { openPlan: () => { sidebar.openTab(ROOM_PLAN_KIND, { params: {} }); } },
396
+ stopExecution: async (name, run) => {
397
+ const result = await remote?.cancel({ sessionId, name, expectedStartedAt: run.startedAt, ...run.runId === undefined ? {} : { expectedRunId: run.runId } });
398
+ await roomStore.refresh(sessionId);
399
+ return result?.ok && result.value.ok && result.value.value.cancelled ? { ok: true } : { ok: false, message: t('activity.stopChanged') };
400
+ },
382
401
  submit,
383
402
  renderMemberConfiguration: ctx.get('localAgentUi')?.renderMemberConfiguration,
384
403
  renderMemberInbox: ctx.get('localAgentUi')?.renderMemberInbox,
385
- stopMember: name => { void remote?.cancel({ sessionId, name }).then(() => roomStore.refresh(sessionId)); },
404
+ stopMember: (name, expectedRunId) => { void remote?.cancel({ sessionId, name, ...expectedRunId === undefined ? {} : { expectedRunId } }).then(() => roomStore.refresh(sessionId)); },
386
405
  modelDirectory: modelDirectoryFor(sessionId),
387
406
  // The hidden official bar's Stop: the runtime session face's cancel
388
407
  // (the same verb ui-conversation's own Stop injects). A torn-down
@@ -394,6 +413,19 @@ export async function apply(ctx) {
394
413
  },
395
414
  }),
396
415
  };
416
+ if (sidebar !== undefined && sidebarTabs !== undefined) {
417
+ const planToggle = new RegistrationToggle(() => sidebarTabs.register({ id: ROOM_PLAN_TAB_ID, kind: ROOM_PLAN_KIND, title: () => t('activity.plan') }), () => roomChrome.show(mainSessionId(ctx.sessions.list.getSnapshot())));
418
+ planToggle.setReady(true);
419
+ const unsubscribe = roomChrome.subscribe(() => planToggle.sync());
420
+ disposers.push(async () => { unsubscribe(); planToggle.setReady(false); });
421
+ ctx.slots.inject('sidebar.right.pane.tab', () => ctx.slots.register({
422
+ name: 'sidebar.right.pane.tab', key: ROOM_PLAN_TAB_ID, locale: NS,
423
+ inject: (sessionId) => {
424
+ const { roomStore, planCommand, openPlanSession, stopMember } = composerEntry.inject(sessionId);
425
+ return { roomStore, planCommand, openPlanSession, stopMember };
426
+ },
427
+ }, RoomPlanTab));
428
+ }
397
429
  let composerDispose;
398
430
  ctx.slots.inject('conversation.composer', () => {
399
431
  composerDispose = ctx.slots.register(composerEntry, RoomComposer);
@@ -1,6 +1,31 @@
1
1
  /** `room` namespace dictionaries. */
2
2
  /** Simplified Chinese dictionary (the key-set source of truth). */
3
3
  export declare const zh: {
4
+ 'activity.status.settled': string;
5
+ 'activity.sessionTotal': string;
6
+ "activity.agents": string;
7
+ "activity.plan": string;
8
+ "activity.planDetails": string;
9
+ "activity.limits": string;
10
+ "activity.session": string;
11
+ "activity.stop": string;
12
+ "activity.stopChanged": string;
13
+ "activity.runningHint": string;
14
+ "activity.notStarted": string;
15
+ "activity.empty": string;
16
+ "activity.planning": string;
17
+ "activity.filter.recent": string;
18
+ "activity.filter.running": string;
19
+ "activity.filter.attention": string;
20
+ "activity.filter.all": string;
21
+ "activity.status.queued": string;
22
+ "activity.status.running": string;
23
+ "activity.status.done": string;
24
+ "activity.status.cancelled": string;
25
+ "activity.status.failed": string;
26
+ "activity.status.uncertain": string;
27
+ "activity.status.submitted": string;
28
+ "activity.status.accepted": string;
4
29
  'recovery.title': string;
5
30
  'recovery.help': string;
6
31
  'recovery.done': string;
@@ -215,6 +240,31 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
215
240
  }
216
241
  /** English dictionary, checked complete against the zh key set. */
217
242
  export declare const en: {
243
+ 'activity.status.settled': string;
244
+ 'activity.sessionTotal': string;
245
+ "activity.agents": string;
246
+ "activity.plan": string;
247
+ "activity.planDetails": string;
248
+ "activity.limits": string;
249
+ "activity.session": string;
250
+ "activity.stop": string;
251
+ "activity.stopChanged": string;
252
+ "activity.runningHint": string;
253
+ "activity.notStarted": string;
254
+ "activity.empty": string;
255
+ "activity.planning": string;
256
+ "activity.filter.recent": string;
257
+ "activity.filter.running": string;
258
+ "activity.filter.attention": string;
259
+ "activity.filter.all": string;
260
+ "activity.status.queued": string;
261
+ "activity.status.running": string;
262
+ "activity.status.done": string;
263
+ "activity.status.cancelled": string;
264
+ "activity.status.failed": string;
265
+ "activity.status.uncertain": string;
266
+ "activity.status.submitted": string;
267
+ "activity.status.accepted": string;
218
268
  'recovery.title': string;
219
269
  'recovery.help': string;
220
270
  'recovery.done': string;
@@ -1,6 +1,31 @@
1
1
  /** `room` namespace dictionaries. */
2
2
  /** Simplified Chinese dictionary (the key-set source of truth). */
3
3
  export const zh = {
4
+ 'activity.status.settled': '已结束',
5
+ 'activity.sessionTotal': '会话累计',
6
+ "activity.agents": "后台 Agent",
7
+ "activity.plan": "当前计划",
8
+ "activity.planDetails": "计划详情与运行限制",
9
+ "activity.limits": "运行限制",
10
+ "activity.session": "查看会话",
11
+ "activity.stop": "中止",
12
+ "activity.stopChanged": "这次执行已结束或已发生变化,请查看最新状态。",
13
+ "activity.runningHint": "{count} 个 Agent 正在运行",
14
+ "activity.notStarted": "尚未开始",
15
+ "activity.empty": "当前没有这类执行记录",
16
+ "activity.planning": "协调者正在整理计划",
17
+ "activity.filter.recent": "最近",
18
+ "activity.filter.running": "进行中",
19
+ "activity.filter.attention": "待处理",
20
+ "activity.filter.all": "全部",
21
+ "activity.status.queued": "排队中",
22
+ "activity.status.running": "运行中",
23
+ "activity.status.done": "已完成",
24
+ "activity.status.cancelled": "已中止",
25
+ "activity.status.failed": "失败",
26
+ "activity.status.uncertain": "待核对",
27
+ "activity.status.submitted": "待验收",
28
+ "activity.status.accepted": "已验收",
4
29
  'recovery.title': '中断的对话需要对账',
5
30
  'recovery.help': '重启前的结果尚未确认,不会自动重试。请查看成员会话并填写核对结论;对账后,已排队的普通对话可以继续,正式目标仍遵守暂停状态。',
6
31
  'recovery.done': '确认已完成',
@@ -207,6 +232,31 @@ export const zh = {
207
232
  };
208
233
  /** English dictionary, checked complete against the zh key set. */
209
234
  export const en = {
235
+ 'activity.status.settled': 'Finished',
236
+ 'activity.sessionTotal': 'Session total',
237
+ "activity.agents": "Background agents",
238
+ "activity.plan": "Current plan",
239
+ "activity.planDetails": "Plan details and limits",
240
+ "activity.limits": "Run limits",
241
+ "activity.session": "View conversation",
242
+ "activity.stop": "Stop",
243
+ "activity.stopChanged": "This execution has ended or changed. Check its latest state.",
244
+ "activity.runningHint": "{count} agents are running",
245
+ "activity.notStarted": "Not started",
246
+ "activity.empty": "No matching executions",
247
+ "activity.planning": "The coordinator is preparing the plan",
248
+ "activity.filter.recent": "Recent",
249
+ "activity.filter.running": "Running",
250
+ "activity.filter.attention": "Needs attention",
251
+ "activity.filter.all": "All",
252
+ "activity.status.queued": "Queued",
253
+ "activity.status.running": "Running",
254
+ "activity.status.done": "Completed",
255
+ "activity.status.cancelled": "Stopped",
256
+ "activity.status.failed": "Failed",
257
+ "activity.status.uncertain": "Needs reconciliation",
258
+ "activity.status.submitted": "Awaiting review",
259
+ "activity.status.accepted": "Accepted",
210
260
  'recovery.title': 'Interrupted conversations need reconciliation',
211
261
  'recovery.help': 'The pre-restart outcome is unknown and will not be retried automatically. Inspect the member session and record your finding. Reconciliation releases queued ordinary conversations; paused goals remain paused.',
212
262
  'recovery.done': 'Confirm completed',
@@ -2,7 +2,7 @@
2
2
  * The preset-composition visibility of room's SESSION chrome (M3' self-hide,
3
3
  * the mode-switcher proposal's "组合里有我的行"): the invite chip and the
4
4
  * members tab show exactly when the CURRENT session's preset composition
5
- * names the `@khorsheed/dsh-room-tool` row — the preset composition file is
5
+ * names the `@khorsheed/dsh-room/tool` row — the preset composition file is
6
6
  * the single source of truth, read from the official `pluginInventory`
7
7
  * Remote (the same criterion the worktrees badge implements; inlined here
8
8
  * rather than extracted into a helper package, per the proposal's M3'
@@ -16,8 +16,8 @@
16
16
  */
17
17
  import type { Context } from '@deepseek-ai/cordis';
18
18
  import type { SessionId } from '@deepseek-ai/dsh-session/types';
19
- /** The composition criterion's row: room's companion tool package. */
20
- export declare const ROOM_TOOL_ROW_MODULE = "@khorsheed/dsh-room-tool";
19
+ /** The composition criterion's row: this package's own `./tool` composition entry. */
20
+ export declare const ROOM_TOOL_ROW_MODULE = "@khorsheed/dsh-room/tool";
21
21
  /**
22
22
  * Minimal structural mirror of the official pluginInventory snapshot — only
23
23
  * the agentPresets slice the criterion reads (duck-typed, host-version
@@ -1,5 +1,5 @@
1
- /** The composition criterion's row: room's companion tool package. */
2
- export const ROOM_TOOL_ROW_MODULE = '@khorsheed/dsh-room-tool';
1
+ /** The composition criterion's row: this package's own `./tool` composition entry. */
2
+ export const ROOM_TOOL_ROW_MODULE = '@khorsheed/dsh-room/tool';
3
3
  /** The current session's agent preset, the badge's dual read per host line. */
4
4
  function presetOf(row) {
5
5
  const value = row
@@ -160,12 +160,17 @@ export interface RoomModelDirectory {
160
160
  * ui-model-selection — the picker simply does not render.
161
161
  */
162
162
  export interface RoomComposerInjected extends RoomTasksInjected, RoomInviteInjected {
163
+ readonly backgroundSessions?: import('./executions.ts').BackgroundSessionsStore | undefined;
164
+ readonly activeChildren?: (() => Promise<readonly string[] | undefined>) | undefined;
165
+ readonly stopChild?: ((childSessionId: string) => Promise<RoomMutationOutcome>) | undefined;
166
+ readonly openPlan?: (() => void) | undefined;
167
+ readonly stopExecution?: ((name: string, run: import('../types.ts').RoomMemberRun) => Promise<RoomMutationOutcome>) | undefined;
163
168
  readonly reconcileDelivery?: ((deliveryId: string, outcome: 'done' | 'cancelled', evidence: string) => Promise<RoomMutationOutcome>) | undefined;
164
169
  readonly planCommand?: ((command: string) => Promise<RoomMutationOutcome>) | undefined;
165
170
  readonly openPlanSession?: ((sessionId: string) => void) | undefined;
166
171
  readonly renderMemberInbox?: LocalAgentUi['renderMemberInbox'] | undefined;
167
172
  readonly renderMemberConfiguration?: LocalAgentUi['renderMemberConfiguration'] | undefined;
168
- readonly stopMember?: ((name: string) => void) | undefined;
173
+ readonly stopMember?: ((name: string, expectedRunId?: string) => void) | undefined;
169
174
  /**
170
175
  * Dispatch an @-message into the room and refresh the store on success.
171
176
  * Bare messages never reach here — the composer releases them to the
@@ -191,6 +191,12 @@ export class DispatchEngine {
191
191
  const error = 'Execution outcome is unknown after restart; reconcile before retrying';
192
192
  return {
193
193
  ...state,
194
+ ...state.executions === undefined ? {} : { executions: state.executions.map(execution => {
195
+ const active = execution.runId === undefined
196
+ ? [...this.inFlight.values()].includes(`${sessionId} ${execution.memberId}`)
197
+ : this.scheduled.has(`${sessionId}:${execution.runId}`);
198
+ return execution.state === 'running' && !active ? { ...execution, state: 'failed', error } : execution;
199
+ }) },
194
200
  ...state.deliveries === undefined ? {} : { deliveries: state.deliveries.map(row => row.status === 'queued' && closedGoal(state, row.plan?.goalId)
195
201
  ? { ...row, status: 'cancelled' } : row.status === 'running' && !this.scheduled.has(`${sessionId}:${row.id}`)
196
202
  ? { ...row, status: 'uncertain', error } : row) },
@@ -269,7 +275,9 @@ export class DispatchEngine {
269
275
  if (deliveryId !== undefined)
270
276
  room.append('room/delivery-state', { id: deliveryId, dispatchSeq: options.dispatchSeq, memberId: identity, state: 'running' });
271
277
  memberName = replay(room.snapshotEvents()).members.find(entry => memberId(room.snapshotEvents(), entry) === identity)?.name ?? memberName;
272
- room.append('room/run-state', { member: memberName, state: 'running', startedAt, runId });
278
+ room.append('room/run-state', { member: memberName, state: 'running', startedAt, runId,
279
+ ...member.kind === 'main-agent' ? { childSessionId: room.id } : member.childSessionId === undefined ? {} : { childSessionId: member.childSessionId },
280
+ });
273
281
  await this.hooks.admitted?.(room, options.dispatchSeq);
274
282
  await this.ctx.sessions.flush(room);
275
283
  return startedAt;
@@ -278,7 +286,7 @@ export class DispatchEngine {
278
286
  try {
279
287
  outcome = member.kind === 'main-agent'
280
288
  ? await this.runMainAgent(room, member, cursor, text, await admitted(), options.relayIds ?? [])
281
- : await this.runCliMember(room, member, cursor, text, admitted, options.relayIds ?? [], submitted);
289
+ : await this.runCliMember(room, member, cursor, text, admitted, options.relayIds ?? [], submitted, runId);
282
290
  }
283
291
  catch (error) {
284
292
  this.ctx.logger.warn(`room: dispatch to "${memberName}" failed: ${String(error)}`);
@@ -351,7 +359,7 @@ export class DispatchEngine {
351
359
  return { state: 'done' };
352
360
  }
353
361
  /** CLI member turn: facade start (first round) or resume, then the speech mirror. */
354
- async runCliMember(room, member, cursor, text, admitted, relayIds, submitted) {
362
+ async runCliMember(room, member, cursor, text, admitted, relayIds, submitted, runId) {
355
363
  const facade = probeLocalAgent(this.ctx);
356
364
  if (facade === undefined) {
357
365
  this.ctx.logger.warn(`room: cannot dispatch to "${member.name}": local-agent facade unavailable`);
@@ -363,7 +371,23 @@ export class DispatchEngine {
363
371
  // configuration admit the request. Persistence must finish before native work.
364
372
  let startedAt;
365
373
  const controlled = facade.supportsMemberConfiguration?.(provider) === true;
366
- const admission = controlled ? { onAdmitted: async () => { startedAt = await admitted(); } } : {};
374
+ const admission = {
375
+ ...controlled ? { onAdmitted: async () => { startedAt = await admitted(); } } : {},
376
+ onProgress: (event) => {
377
+ if (event.kind !== 'settled')
378
+ return;
379
+ const previous = room.snapshotEvents().filter(row => row.type === 'room/run-state' && row.data.runId === runId).at(-1);
380
+ if (previous?.type !== 'room/run-state')
381
+ return;
382
+ const total = event.usage?.totalTokens ?? (event.usage === undefined ? undefined : event.usage.inputTokens + event.usage.outputTokens);
383
+ room.append('room/execution-metadata', { runId,
384
+ ...event.observedModel === undefined ? {} : { model: event.observedModel },
385
+ ...event.observedEffort === undefined ? {} : { effort: event.observedEffort },
386
+ ...total === undefined || !Number.isFinite(total) || total < 0 ? {} : { tokens: total },
387
+ });
388
+ void this.ctx.sessions.flush(room).catch(error => this.ctx.logger.warn(`room: execution metadata flush failed: ${String(error)}`));
389
+ },
390
+ };
367
391
  if (!controlled)
368
392
  startedAt = await admitted();
369
393
  const { prompt, carried } = assemblePrompt(room, member, cursor, text, relayIds);
@@ -374,8 +398,8 @@ export class DispatchEngine {
374
398
  // The invite-time model lands as the delegation's own model: the facade
375
399
  // records it with the first start and every later resume re-requests it
376
400
  // (providers bind it at spawn for exec and live alike).
377
- ? facade.start(room.id, provider, [{ type: 'text', text: prompt }], member.model === undefined && member.cwd === undefined && !controlled ? undefined : { ...admission, ...member.model === undefined ? {} : { model: member.model }, ...member.cwd === undefined ? {} : { cwd: member.cwd } })
378
- : facade.resume(room.id, provider, member.childSessionId, [{ type: 'text', text: prompt }], member.cwd === undefined && !controlled ? undefined : { ...admission, ...member.cwd === undefined ? {} : { cwd: member.cwd } });
401
+ ? facade.start(room.id, provider, [{ type: 'text', text: prompt }], { ...admission, ...member.model === undefined ? {} : { model: member.model }, ...member.cwd === undefined ? {} : { cwd: member.cwd } })
402
+ : facade.resume(room.id, provider, member.childSessionId, [{ type: 'text', text: prompt }], { ...admission, ...member.cwd === undefined ? {} : { cwd: member.cwd } });
379
403
  // Existing members can enter core's queue immediately. A fresh/prepared
380
404
  // identity must publish its first handle before the next submission resumes it.
381
405
  if (controlled && member.childSessionId !== undefined && !prepared)
@@ -296,6 +296,11 @@ export declare class RoomService extends TypertRemoteService {
296
296
  * @returns the cancellation receipt, or a rejection.
297
297
  */
298
298
  cancel(request: RoomCancelRequest): Promise<RoomCancelResult>;
299
+ /** Native descendant controls share the host's continuation and cancellation channels. */
300
+ cancelChild(request: {
301
+ sessionId: SessionId;
302
+ childSessionId: SessionId;
303
+ }): Promise<RoomCancelResult>;
299
304
  }
300
305
  export default RoomService;
301
306
  //# sourceMappingURL=index.d.ts.map
@@ -44,8 +44,8 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
44
44
  * room host half: the `room` Typert Remote service. (The `room_invite` /
45
45
  * `room_task` / `room_message` model tools left this package's profile-root
46
46
  * registration in the M4' tool-row split — their definition factories live
47
- * in ./tool.ts and the companion `@khorsheed/dsh-room-tool` mounts them
48
- * inside agent-preset compositions.) The room's entire state is the session's `room/*` custom-event
47
+ * in ./tool-definition.ts and the package's own `./tool` composition entry
48
+ * mounts them inside agent-preset compositions.) The room's entire state is the session's `room/*` custom-event
49
49
  * journal (log-only events — persistence and reload-replay come free, the
50
50
  * model never sees them, and harnesses without this plugin replay the session
51
51
  * safely); every read folds the journal through the pure replay, and every
@@ -126,6 +126,7 @@ let RoomService = (() => {
126
126
  let _closeTask_decorators;
127
127
  let _setGoal_decorators;
128
128
  let _cancel_decorators;
129
+ let _cancelChild_decorators;
129
130
  return class RoomService extends _classSuper {
130
131
  static {
131
132
  const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
@@ -146,6 +147,7 @@ let RoomService = (() => {
146
147
  _closeTask_decorators = [Remote('closeTask')];
147
148
  _setGoal_decorators = [Remote('setGoal')];
148
149
  _cancel_decorators = [Remote('cancel')];
150
+ _cancelChild_decorators = [Remote('cancelChild')];
149
151
  __esDecorate(this, null, _isRoom_decorators, { kind: "method", name: "isRoom", static: false, private: false, access: { has: obj => "isRoom" in obj, get: obj => obj.isRoom }, metadata: _metadata }, null, _instanceExtraInitializers);
150
152
  __esDecorate(this, null, _listProviders_decorators, { kind: "method", name: "listProviders", static: false, private: false, access: { has: obj => "listProviders" in obj, get: obj => obj.listProviders }, metadata: _metadata }, null, _instanceExtraInitializers);
151
153
  __esDecorate(this, null, _getState_decorators, { kind: "method", name: "getState", static: false, private: false, access: { has: obj => "getState" in obj, get: obj => obj.getState }, metadata: _metadata }, null, _instanceExtraInitializers);
@@ -163,6 +165,7 @@ let RoomService = (() => {
163
165
  __esDecorate(this, null, _closeTask_decorators, { kind: "method", name: "closeTask", static: false, private: false, access: { has: obj => "closeTask" in obj, get: obj => obj.closeTask }, metadata: _metadata }, null, _instanceExtraInitializers);
164
166
  __esDecorate(this, null, _setGoal_decorators, { kind: "method", name: "setGoal", static: false, private: false, access: { has: obj => "setGoal" in obj, get: obj => obj.setGoal }, metadata: _metadata }, null, _instanceExtraInitializers);
165
167
  __esDecorate(this, null, _cancel_decorators, { kind: "method", name: "cancel", static: false, private: false, access: { has: obj => "cancel" in obj, get: obj => obj.cancel }, metadata: _metadata }, null, _instanceExtraInitializers);
168
+ __esDecorate(this, null, _cancelChild_decorators, { kind: "method", name: "cancelChild", static: false, private: false, access: { has: obj => "cancelChild" in obj, get: obj => obj.cancelChild }, metadata: _metadata }, null, _instanceExtraInitializers);
166
169
  if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
167
170
  }
168
171
  static inject = ['sessions', 'agents'];
@@ -232,9 +235,10 @@ let RoomService = (() => {
232
235
  return { kind: 'enter', messages: [] };
233
236
  });
234
237
  // The model-facing room tools are deliberately NOT registered here: they
235
- // moved to the companion `@khorsheed/dsh-room-tool`, which mounts the
236
- // tool row inside agent-preset compositions (session-granted). The
237
- // definition factories stay exported from ./tool.ts for that companion.
238
+ // belong to this package's `./tool` composition entry (src/tool.ts),
239
+ // which mounts the tool row inside agent-preset compositions
240
+ // (session-granted). The definition factories live in
241
+ // ./tool-definition.ts for that row.
238
242
  }
239
243
  /** Resolve a LIVE session as a room: presence, marker, replayed state. */
240
244
  load(sessionId) {
@@ -1169,16 +1173,25 @@ let RoomService = (() => {
1169
1173
  const member = loaded.state.members.find(entry => entry.name === request.name);
1170
1174
  if (member === undefined)
1171
1175
  return { ok: false, error: { code: 'member-not-found' } };
1176
+ const expected = loaded.state.runs.find(entry => entry.member === request.name && entry.state === 'running');
1177
+ if ((request.expectedRunId !== undefined && expected?.runId !== request.expectedRunId)
1178
+ || (request.expectedStartedAt !== undefined && expected?.startedAt !== request.expectedStartedAt)) {
1179
+ return { ok: true, value: { cancelled: false } };
1180
+ }
1172
1181
  const facade = probeLocalAgent(this.ctx);
1173
- const hit = facade !== undefined && member.childSessionId !== undefined
1182
+ const native = member.kind === 'main-agent' && expected !== undefined ? this.ctx.agents.get(loaded.session.id) : undefined;
1183
+ if (native !== undefined)
1184
+ native.cancel({ kind: 'user' });
1185
+ const hit = native !== undefined || (facade !== undefined && member.childSessionId !== undefined
1174
1186
  ? facade.cancel(member.childSessionId)
1175
- : false;
1187
+ : false);
1176
1188
  if (hit) {
1177
1189
  const running = loaded.state.runs.find(entry => entry.member === request.name && entry.state === 'running');
1178
1190
  if (running === undefined)
1179
1191
  return { ok: true, value: { cancelled: true } };
1180
1192
  loaded.session.append('room/run-state', {
1181
1193
  member: request.name, state: 'cancelled', startedAt: running.startedAt,
1194
+ elapsedMs: Math.max(0, Date.now() - running.startedAt),
1182
1195
  ...running.runId === undefined ? {} : { runId: running.runId },
1183
1196
  });
1184
1197
  // The engine's settle no-ops behind this edge, so the task closing the
@@ -1194,6 +1207,28 @@ let RoomService = (() => {
1194
1207
  }
1195
1208
  return { ok: true, value: { cancelled: false } };
1196
1209
  }
1210
+ /** Native descendant controls share the host's continuation and cancellation channels. */
1211
+ async cancelChild(request) {
1212
+ const loaded = await this.ensureLive(request.sessionId);
1213
+ if (!loaded.ok)
1214
+ return { ok: false, error: loaded.error };
1215
+ const target = this.ctx.sessions.get(request.childSessionId);
1216
+ const seen = new Set([request.childSessionId]);
1217
+ let parent = target?.header.parentSession;
1218
+ while (parent !== undefined && parent !== request.sessionId && !seen.has(parent)) {
1219
+ seen.add(parent);
1220
+ parent = this.ctx.sessions.get(parent)?.header.parentSession;
1221
+ }
1222
+ if (!target || target.id === request.sessionId || parent !== request.sessionId)
1223
+ return { ok: true, value: { cancelled: false } };
1224
+ const facadeHit = probeLocalAgent(this.ctx)?.cancel(target.id) ?? false;
1225
+ const agent = this.ctx.agents.get(target.id);
1226
+ if (agent) {
1227
+ this.ctx.get('subagents')?.interrupt(target.id, { kind: 'user', parentSessionId: target.header.parentSession });
1228
+ agent.cancel({ kind: 'user' });
1229
+ }
1230
+ return { ok: true, value: { cancelled: facadeHit || agent !== undefined } };
1231
+ }
1197
1232
  };
1198
1233
  })();
1199
1234
  export { RoomService };
@@ -18,7 +18,7 @@ export declare const MAIN_AGENT_MEMBER = "dsh";
18
18
  * in-place add is that surface's temporary form — migrate to the official
19
19
  * one when it lands upstream.
20
20
  */
21
- export declare const ROOM_EVENT_TYPES: readonly ['room/created', 'room/plan-state', 'room/coordinator', 'room/delivery-state', 'room/member-added', 'room/member-updated', 'room/member-removed', 'room/dispatch', 'room/speech', 'room/run-state', 'room/relay', 'room/relay-resolved', 'room/task-added', 'room/task-updated', 'room/task-edited', 'room/goal'];
21
+ export declare const ROOM_EVENT_TYPES: readonly ['room/created', 'room/plan-state', 'room/coordinator', 'room/delivery-state', 'room/member-added', 'room/member-updated', 'room/member-removed', 'room/dispatch', 'room/speech', 'room/run-state', 'room/execution-metadata', 'room/relay', 'room/relay-resolved', 'room/task-added', 'room/task-updated', 'room/task-edited', 'room/goal'];
22
22
  /** Whether an event log carries the room identity marker. */
23
23
  export declare function isRoomLog(events: readonly SessionEvent[]): boolean;
24
24
  /**