@khorsheed/dsh-room 0.2.2 → 0.2.4

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 (109) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.en.md +29 -62
  3. package/README.i18n.yaml +2 -2
  4. package/README.md +28 -61
  5. package/lib/client.js +14620 -5705
  6. package/lib/index.js +4069 -730
  7. package/lib/journal-B4FOGO6S.js +482 -0
  8. package/lib/journal-Rid9AAnD.js +485 -0
  9. package/lib/tool.js +112 -166
  10. package/lib/typert.host.js +2031 -374
  11. package/lib/typert.remote-client.d.ts +34 -10
  12. package/lib/typert.remote-client.js +1599 -210
  13. package/lib/types/agent-setup.d.ts +1 -1
  14. package/lib/types/client/InviteAgentAction.js +4 -16
  15. package/lib/types/client/InviteDialog.d.ts +28 -6
  16. package/lib/types/client/InviteDialog.js +76 -12
  17. package/lib/types/client/MemberCard.d.ts +5 -1
  18. package/lib/types/client/MemberCard.js +4 -4
  19. package/lib/types/client/MembersView.d.ts +1 -1
  20. package/lib/types/client/MembersView.js +58 -22
  21. package/lib/types/client/RemoveMemberConfirm.d.ts +40 -0
  22. package/lib/types/client/RemoveMemberConfirm.js +85 -0
  23. package/lib/types/client/RoomApprovalView.d.ts +11 -0
  24. package/lib/types/client/RoomApprovalView.js +28 -0
  25. package/lib/types/client/RoomComposer.d.ts +1 -1
  26. package/lib/types/client/RoomComposer.js +195 -29
  27. package/lib/types/client/RoomDecisionView.d.ts +22 -0
  28. package/lib/types/client/RoomDecisionView.js +35 -0
  29. package/lib/types/client/RoomHelpView.d.ts +17 -0
  30. package/lib/types/client/RoomHelpView.js +56 -0
  31. package/lib/types/client/RoomInboxView.d.ts +10 -0
  32. package/lib/types/client/RoomInboxView.js +14 -0
  33. package/lib/types/client/RoomQuestionView.d.ts +13 -0
  34. package/lib/types/client/RoomQuestionView.js +61 -0
  35. package/lib/types/client/RoomRelayView.js +1 -1
  36. package/lib/types/client/RoomRunView.d.ts +3 -1
  37. package/lib/types/client/RoomRunView.js +12 -5
  38. package/lib/types/client/RoomSettingsView.d.ts +19 -0
  39. package/lib/types/client/RoomSettingsView.js +30 -0
  40. package/lib/types/client/RoomSpeechView.d.ts +22 -8
  41. package/lib/types/client/RoomSpeechView.js +94 -11
  42. package/lib/types/client/RoomStatsLine.d.ts +26 -5
  43. package/lib/types/client/RoomStatsLine.js +53 -7
  44. package/lib/types/client/RoomTaskCapsule.d.ts +19 -0
  45. package/lib/types/client/RoomTaskCapsule.js +37 -0
  46. package/lib/types/client/TasksTab.d.ts +41 -0
  47. package/lib/types/client/TasksTab.js +43 -0
  48. package/lib/types/client/TasksView.d.ts +44 -0
  49. package/lib/types/client/TasksView.js +149 -0
  50. package/lib/types/client/TeamView.d.ts +33 -0
  51. package/lib/types/client/TeamView.js +210 -0
  52. package/lib/types/client/at-menu.d.ts +127 -0
  53. package/lib/types/client/at-menu.js +173 -0
  54. package/lib/types/client/draft-chips.d.ts +67 -0
  55. package/lib/types/client/draft-chips.js +95 -0
  56. package/lib/types/client/executions.d.ts +0 -30
  57. package/lib/types/client/executions.js +4 -34
  58. package/lib/types/client/icons-local.d.ts +25 -0
  59. package/lib/types/client/icons-local.js +10 -0
  60. package/lib/types/client/icons.d.ts +2 -1
  61. package/lib/types/client/icons.js +1 -0
  62. package/lib/types/client/index.js +276 -72
  63. package/lib/types/client/locales.d.ts +482 -52
  64. package/lib/types/client/locales.js +490 -60
  65. package/lib/types/client/nodes.d.ts +112 -17
  66. package/lib/types/client/nodes.js +197 -26
  67. package/lib/types/client/slots.d.ts +117 -47
  68. package/lib/types/client/task-tag.d.ts +32 -0
  69. package/lib/types/client/task-tag.js +33 -0
  70. package/lib/types/client/tasks.d.ts +50 -0
  71. package/lib/types/client/tasks.js +82 -0
  72. package/lib/types/client/team.d.ts +73 -0
  73. package/lib/types/client/team.js +187 -0
  74. package/lib/types/decisions.d.ts +49 -0
  75. package/lib/types/decisions.js +101 -0
  76. package/lib/types/deliveries.d.ts +17 -1
  77. package/lib/types/deliveries.js +82 -6
  78. package/lib/types/dispatch.d.ts +102 -4
  79. package/lib/types/dispatch.js +498 -109
  80. package/lib/types/help.d.ts +137 -0
  81. package/lib/types/help.js +216 -0
  82. package/lib/types/helps.d.ts +26 -0
  83. package/lib/types/helps.js +47 -0
  84. package/lib/types/index.d.ts +292 -52
  85. package/lib/types/index.js +1421 -199
  86. package/lib/types/jobs.d.ts +22 -0
  87. package/lib/types/jobs.js +77 -0
  88. package/lib/types/journal.d.ts +17 -27
  89. package/lib/types/journal.js +120 -43
  90. package/lib/types/messaging.d.ts +65 -0
  91. package/lib/types/messaging.js +160 -0
  92. package/lib/types/plan-service.js +7 -1
  93. package/lib/types/report-gate.d.ts +81 -0
  94. package/lib/types/report-gate.js +89 -0
  95. package/lib/types/tool-definition.d.ts +43 -20
  96. package/lib/types/tool-definition.js +145 -212
  97. package/lib/types/tool.d.ts +2 -2
  98. package/lib/types/tool.js +3 -2
  99. package/lib/types/types.d.ts +755 -78
  100. package/locale/zh.json +1 -1
  101. package/package.json +3 -3
  102. package/lib/types/client/RoomActivityDock.d.ts +0 -19
  103. package/lib/types/client/RoomActivityDock.js +0 -105
  104. package/lib/types/client/RoomDockCapsules.d.ts +0 -64
  105. package/lib/types/client/RoomDockCapsules.js +0 -277
  106. package/lib/types/client/RoomPlanTab.d.ts +0 -15
  107. package/lib/types/client/RoomPlanTab.js +0 -13
  108. package/lib/types/client/RoomTaskLineView.d.ts +0 -13
  109. package/lib/types/client/RoomTaskLineView.js +0 -21
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Tasks (jobs): what the human asked for and everything it set in motion.
3
+ * A task starts at a human message that led to work — at least one
4
+ * delivery carries its id — and gathers the follow-up messages attributed to
5
+ * it (`room/task-link`, by the composer's tag, the coordinator or the
6
+ * decision model) and every delivery recorded under it: delegations,
7
+ * members' sub-delegations, reports. Plain chat that dispatched nothing is
8
+ * not a task. Pure: the host replay and the client views share it.
9
+ * @module @khorsheed/dsh-room/jobs
10
+ */
11
+ import type { SessionEvent } from '@deepseek-ai/dsh-session/types';
12
+ import type { RoomDelivery, RoomJob } from './types.ts';
13
+ /**
14
+ * Fold the room log into its tasks, newest first.
15
+ * @param events - the room log.
16
+ * @param deliveries - the replayed deliveries (their `task` already resolved).
17
+ * @returns the tasks.
18
+ */
19
+ export declare function replayJobs(events: readonly SessionEvent[], deliveries: readonly RoomDelivery[]): RoomJob[];
20
+ /** Resolve the coordinator's `task` argument (`new`, a 「任务 N」 number, or a root id) against the room's tasks. */
21
+ export declare function resolveTaskRef(jobs: readonly RoomJob[], ref: string): string | 'new' | undefined;
22
+ //# sourceMappingURL=jobs.d.ts.map
@@ -0,0 +1,77 @@
1
+ import { isHumanMessage } from './deliveries.js';
2
+ /** The text of a message's text blocks. */
3
+ function textOf(content) {
4
+ return content.flatMap(block => block.type === 'text' && typeof block.text === 'string' ? [block.text] : []).join('\n');
5
+ }
6
+ /**
7
+ * Fold the room log into its tasks, newest first.
8
+ * @param events - the room log.
9
+ * @param deliveries - the replayed deliveries (their `task` already resolved).
10
+ * @returns the tasks.
11
+ */
12
+ export function replayJobs(events, deliveries) {
13
+ const messages = new Map();
14
+ const links = new Map();
15
+ const stopped = new Set();
16
+ const dispatchById = new Map();
17
+ for (const event of events) {
18
+ if (isHumanMessage(event))
19
+ messages.set(event.data.id, { id: event.data.id, time: event.time, text: textOf(event.data.content) });
20
+ else if (event.type === 'room/task-link') {
21
+ links.set(event.data.message, { task: event.data.task, by: event.data.by, ...event.data.probability === undefined ? {} : { probability: event.data.probability } });
22
+ }
23
+ else if (event.type === 'room/job-stopped')
24
+ stopped.add(event.data.task);
25
+ else if (event.type === 'room/dispatch' && event.data.id !== undefined)
26
+ dispatchById.set(event.data.id, { time: event.time, text: event.data.text });
27
+ }
28
+ const byTask = new Map();
29
+ for (const row of deliveries) {
30
+ if (row.task === undefined)
31
+ continue;
32
+ const list = byTask.get(row.task) ?? [];
33
+ list.push(row);
34
+ byTask.set(row.task, list);
35
+ }
36
+ const roots = [...byTask.keys()]
37
+ .map(id => ({ id, root: messages.get(id) ?? dispatchById.get(id) }))
38
+ .filter((entry) => entry.root !== undefined)
39
+ .sort((a, b) => a.root.time - b.root.time);
40
+ const jobs = roots.map(({ id, root }, index) => {
41
+ const rows = byTask.get(id) ?? [];
42
+ const follow = [...links.entries()]
43
+ .filter(([message, link]) => link.task === id && message !== id)
44
+ .flatMap(([message, link]) => {
45
+ const found = messages.get(message);
46
+ return found === undefined ? [] : [{ ...found, by: link.by, ...link.probability === undefined ? {} : { probability: link.probability } }];
47
+ });
48
+ const open = rows.some(row => row.status === 'queued' || row.status === 'running' || row.status === 'held');
49
+ const attention = rows.some(row => row.status === 'uncertain');
50
+ // A round with an unknown outcome needs the human even in a stopped task.
51
+ const status = attention ? 'attention' : stopped.has(id) && !open ? 'stopped' : open ? 'running' : 'done';
52
+ const ended = Math.max(...rows.map(row => row.endedAt ?? row.queuedAt));
53
+ const goal = rows.find(row => row.plan !== undefined)?.plan?.goalId;
54
+ return {
55
+ id, number: index + 1,
56
+ // Leading @-mentions address members; the request is what follows.
57
+ title: (root.text.replace(/^(?:@\S+\s+)+/, '').split('\n', 1)[0] ?? '').slice(0, 120),
58
+ messages: [{ id, time: root.time, text: root.text }, ...follow].sort((a, b) => a.time - b.time),
59
+ status, startedAt: root.time,
60
+ ...open ? {} : { endedAt: ended },
61
+ deliveries: rows.map(row => row.id),
62
+ members: [...new Set(rows.map(row => row.memberId))],
63
+ ...goal === undefined ? {} : { goalId: goal },
64
+ };
65
+ });
66
+ return jobs.reverse();
67
+ }
68
+ /** Resolve the coordinator's `task` argument (`new`, a 「任务 N」 number, or a root id) against the room's tasks. */
69
+ export function resolveTaskRef(jobs, ref) {
70
+ const value = ref.trim().replace(/^任务\s*/, '').replace(/^#/, '');
71
+ if (value === 'new' || value === '新')
72
+ return 'new';
73
+ const number = Number(value);
74
+ if (Number.isInteger(number) && number > 0)
75
+ return jobs.find(job => job.number === number)?.id;
76
+ return jobs.find(job => job.id === value)?.id;
77
+ }
@@ -1,11 +1,13 @@
1
1
  import type { SessionEvent } from '@deepseek-ai/dsh-session/types';
2
- import type { RoomMember, RoomState, RoomTask, RoomTaskProgress } from './types.ts';
2
+ import type { RoomMember, RoomSettings, RoomState, RoomTask, RoomTaskProgress } from './types.ts';
3
3
  /**
4
4
  * The native DSH member's addressing name in new rooms. It starts as the
5
5
  * coordinator; bare messages follow the selected coordinator thereafter.
6
6
  * Existing journals retain their recorded names, including legacy main.
7
7
  */
8
8
  export declare const MAIN_AGENT_MEMBER = "dsh";
9
+ /** Longest accepted public member role (`RoomMember.role`), in characters. */
10
+ export declare const ROOM_ROLE_MAX = 30;
9
11
  /**
10
12
  * Every `room/*` session-event type this package introduces. Room registers
11
13
  * them into the harness's KNOWN_SESSION_EVENT_TYPES persistence catalog at
@@ -18,7 +20,9 @@ export declare const MAIN_AGENT_MEMBER = "dsh";
18
20
  * in-place add is that surface's temporary form — migrate to the official
19
21
  * one when it lands upstream.
20
22
  */
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'];
23
+ 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/approval', 'room/approval-resolved', 'room/question', 'room/question-resolved', 'room/task-added', 'room/task-updated', 'room/task-edited', 'room/goal', 'room/settings', 'room/report-gate', 'room/decision', 'room/task-link', 'room/job-stopped', 'room/help', 'room/help-message', 'room/help-state', 'room/help-in', 'room/help-link', 'room/help-unlink', 'room/help-nudge-dismissed'];
24
+ /** Collaboration settings of a room that never changed them. */
25
+ export declare const DEFAULT_ROOM_SETTINGS: RoomSettings;
22
26
  /** Whether an event log carries the room identity marker. */
23
27
  export declare function isRoomLog(events: readonly SessionEvent[]): boolean;
24
28
  /**
@@ -32,20 +36,6 @@ export declare function parseMentions(raw: string): {
32
36
  readonly targets: readonly string[];
33
37
  readonly text: string;
34
38
  };
35
- /**
36
- * The fallback notification channel: a member reply's TRAILING own line of
37
- * the form `@name <content>` (the format the roster injection teaches).
38
- * Only the last line counts — an `@name` inside prose is a mention, not a
39
- * notification. Leading whitespace on that line is tolerated (models indent
40
- * under their own bullet/list style). The caller checks the target against
41
- * the roster.
42
- * @param text - the member's reply text.
43
- * @returns the parsed directive, or undefined.
44
- */
45
- export declare function parseRelayDirective(text: string): {
46
- readonly to: string;
47
- readonly content: string;
48
- } | undefined;
49
39
  /**
50
40
  * Fold a session's event log into the room state.
51
41
  *
@@ -81,20 +71,20 @@ export declare function taskProgress(tasks: readonly RoomTask[]): RoomTaskProgre
81
71
  */
82
72
  export declare function previousCursor(events: readonly SessionEvent[], member: string, beforeSeq: number): number | undefined;
83
73
  /**
84
- * Whether the roster a member was last told about is stale: any roster-visible
85
- * change with a seq past their dispatch cursor — a member-added or
86
- * member-removed, or a member-updated that touches what the roster section
87
- * shows (instructions = the one-line role, rename = the addressing name). A
88
- * childSessionId/cwd/model-only update is NOT roster-visible (the delegation
89
- * handle is journaled right after a first run; counting it would re-send the
90
- * roster on every member's next dispatch for nothing — the model intent never
91
- * enters any prompt either). A member with no cursor has
92
- * never seen the roster, so their first dispatch always counts as stale.
74
+ * What changed in the room since a member's last dispatch, as one-line
75
+ * notes for the next prompt: other members joining or leaving, renames, role
76
+ * changes, the member's own role change, and a goal change. The full roster
77
+ * rides only the first dispatch's briefing; later dispatches carry these
78
+ * deltas instead (the member's own session holds the rest), and a member who
79
+ * wants the whole picture calls `room_read`. Model, cwd and delegation-handle
80
+ * updates never reach a prompt. A member with no cursor gets no deltas — the
81
+ * briefing already covers everything.
93
82
  * @param events - the session's event log.
94
83
  * @param cursor - the member's pre-dispatch cursor ({@link previousCursor}).
95
- * @returns true when the next dispatch must carry the roster section.
84
+ * @param self - the member's CURRENT name (its own rename is not news to it).
85
+ * @returns the change notes (empty when nothing changed).
96
86
  */
97
- export declare function rosterStaleSince(events: readonly SessionEvent[], cursor: number | undefined): boolean;
87
+ export declare function roomChangesSince(events: readonly SessionEvent[], cursor: number | undefined, self: string): readonly string[];
98
88
  /**
99
89
  * The role-instructions carry for the next dispatch to a member: 'initial'
100
90
  * when the member has never been dispatched (their instructions open the
@@ -11,12 +11,16 @@
11
11
  * @module @khorsheed/dsh-room/journal
12
12
  */
13
13
  import { replayDeliveries } from './deliveries.js';
14
+ import { replayJobs } from './jobs.js';
15
+ import { replayHelps, replayLinks } from './helps.js';
14
16
  /**
15
17
  * The native DSH member's addressing name in new rooms. It starts as the
16
18
  * coordinator; bare messages follow the selected coordinator thereafter.
17
19
  * Existing journals retain their recorded names, including legacy main.
18
20
  */
19
21
  export const MAIN_AGENT_MEMBER = 'dsh';
22
+ /** Longest accepted public member role (`RoomMember.role`), in characters. */
23
+ export const ROOM_ROLE_MAX = 30;
20
24
  /**
21
25
  * Every `room/*` session-event type this package introduces. Room registers
22
26
  * them into the harness's KNOWN_SESSION_EVENT_TYPES persistence catalog at
@@ -43,11 +47,29 @@ export const ROOM_EVENT_TYPES = [
43
47
  'room/execution-metadata',
44
48
  'room/relay',
45
49
  'room/relay-resolved',
50
+ 'room/approval',
51
+ 'room/approval-resolved',
52
+ 'room/question',
53
+ 'room/question-resolved',
46
54
  'room/task-added',
47
55
  'room/task-updated',
48
56
  'room/task-edited',
49
57
  'room/goal',
58
+ 'room/settings',
59
+ 'room/report-gate',
60
+ 'room/decision',
61
+ 'room/task-link',
62
+ 'room/job-stopped',
63
+ 'room/help',
64
+ 'room/help-message',
65
+ 'room/help-state',
66
+ 'room/help-in',
67
+ 'room/help-link',
68
+ 'room/help-unlink',
69
+ 'room/help-nudge-dismissed',
50
70
  ];
71
+ /** Collaboration settings of a room that never changed them. */
72
+ export const DEFAULT_ROOM_SETTINGS = { memberMessages: 'free', hopLimit: 10, paused: false, reportTiming: 'judge', autoTaskFiling: true, reportThreshold: 0.5, reportWaitMinutes: 3, helpReminderMinutes: 30 };
51
73
  /** Whether an event log carries the room identity marker. */
52
74
  export function isRoomLog(events) {
53
75
  return events.some(event => event.type === 'room/created');
@@ -64,7 +86,9 @@ export function parseMentions(raw) {
64
86
  let rest = raw.trim();
65
87
  for (;;) {
66
88
  const match = /^@(\S+)(?:\s+|$)/.exec(rest);
67
- if (match === null)
89
+ // A host reference (`@[title](dsh-session:…)`, the @ menu's session chip)
90
+ // names another session, never a member: it stays in the text.
91
+ if (match === null || match[1].startsWith('['))
68
92
  break;
69
93
  const name = match[1];
70
94
  if (!targets.includes(name))
@@ -73,26 +97,6 @@ export function parseMentions(raw) {
73
97
  }
74
98
  return { targets, text: rest };
75
99
  }
76
- /**
77
- * The fallback notification channel: a member reply's TRAILING own line of
78
- * the form `@name <content>` (the format the roster injection teaches).
79
- * Only the last line counts — an `@name` inside prose is a mention, not a
80
- * notification. Leading whitespace on that line is tolerated (models indent
81
- * under their own bullet/list style). The caller checks the target against
82
- * the roster.
83
- * @param text - the member's reply text.
84
- * @returns the parsed directive, or undefined.
85
- */
86
- export function parseRelayDirective(text) {
87
- const trimmed = text.trimEnd();
88
- if (trimmed === '')
89
- return undefined;
90
- const last = trimmed.slice(trimmed.lastIndexOf('\n') + 1).trimStart();
91
- const match = /^@(\S+)\s+(\S[\s\S]*)$/.exec(last);
92
- if (match === null)
93
- return undefined;
94
- return { to: match[1], content: match[2].trim() };
95
- }
96
100
  /**
97
101
  * Fold a session's event log into the room state.
98
102
  *
@@ -111,15 +115,26 @@ export function replay(events) {
111
115
  const members = [];
112
116
  const byName = new Map();
113
117
  const relays = [];
118
+ const approvals = new Map();
119
+ const questions = new Map();
114
120
  const relayById = new Map();
115
121
  const tasks = [];
116
122
  const taskById = new Map();
117
123
  const runs = new Map();
118
124
  const executions = new Map();
119
125
  let goal;
126
+ let settings = DEFAULT_ROOM_SETTINGS;
120
127
  let coordinator;
128
+ let latestReplySeq;
129
+ let latestSpeechSeq;
121
130
  for (const event of events) {
122
131
  switch (event.type) {
132
+ case 'room/speech':
133
+ latestSpeechSeq = event.seq;
134
+ // A reply to the room (not a report to another member).
135
+ if (event.data.reportedTo === undefined)
136
+ latestReplySeq = event.seq;
137
+ break;
123
138
  case 'room/coordinator': {
124
139
  if (event.data.revision > (coordinator?.revision ?? 0))
125
140
  coordinator = event.data;
@@ -135,8 +150,11 @@ export function replay(events) {
135
150
  invitedBy: event.data.invitedBy,
136
151
  ...event.data.provider === undefined ? {} : { provider: event.data.provider },
137
152
  ...event.data.instructions === undefined ? {} : { instructions: event.data.instructions },
153
+ ...event.data.role === undefined ? {} : { role: event.data.role },
154
+ ...event.data.preset === undefined ? {} : { preset: event.data.preset },
138
155
  ...event.data.cwd === undefined ? {} : { cwd: event.data.cwd },
139
156
  ...event.data.model === undefined ? {} : { model: event.data.model },
157
+ ...event.data.effort === undefined ? {} : { effort: event.data.effort },
140
158
  ...event.data.childSessionId === undefined ? {} : { childSessionId: event.data.childSessionId },
141
159
  };
142
160
  byName.set(added.name, added);
@@ -152,14 +170,20 @@ export function replay(events) {
152
170
  const withoutInstructions = event.data.instructions === null
153
171
  ? (({ instructions: _cleared, ...rest }) => rest)(member)
154
172
  : { ...member, ...event.data.instructions === undefined ? {} : { instructions: event.data.instructions } };
173
+ const withoutRole = event.data.role === null
174
+ ? (({ role: _cleared, ...rest }) => rest)(withoutInstructions)
175
+ : { ...withoutInstructions, ...event.data.role === undefined ? {} : { role: event.data.role } };
155
176
  const withoutCwd = event.data.cwd === null
156
- ? (({ cwd: _cleared, ...rest }) => rest)(withoutInstructions)
157
- : { ...withoutInstructions, ...event.data.cwd === undefined ? {} : { cwd: event.data.cwd } };
177
+ ? (({ cwd: _cleared, ...rest }) => rest)(withoutRole)
178
+ : { ...withoutRole, ...event.data.cwd === undefined ? {} : { cwd: event.data.cwd } };
158
179
  const withoutModel = event.data.model === null
159
180
  ? (({ model: _cleared, ...rest }) => rest)(withoutCwd)
160
181
  : { ...withoutCwd, ...event.data.model === undefined ? {} : { model: event.data.model } };
182
+ const withoutEffort = event.data.effort === null
183
+ ? (({ effort: _cleared, ...rest }) => rest)(withoutModel)
184
+ : { ...withoutModel, ...event.data.effort === undefined ? {} : { effort: event.data.effort } };
161
185
  const patched = {
162
- ...withoutModel,
186
+ ...withoutEffort,
163
187
  ...event.data.childSessionId === undefined ? {} : { childSessionId: event.data.childSessionId },
164
188
  };
165
189
  // First admission can precede publication of the native child handle.
@@ -233,11 +257,25 @@ export function replay(events) {
233
257
  content: event.data.content,
234
258
  state: 'pending',
235
259
  ...event.data.provenance === undefined ? {} : { provenance: event.data.provenance },
260
+ ...event.data.reason === undefined ? {} : { reason: event.data.reason },
261
+ ...event.data.intent === undefined ? {} : { intent: event.data.intent },
236
262
  };
237
263
  relayById.set(relay.id, relay);
238
264
  relays.push(relay);
239
265
  break;
240
266
  }
267
+ case 'room/approval':
268
+ approvals.set(event.data.id, { id: event.data.id, member: event.data.member, tool: event.data.tool, ...event.data.reason === undefined ? {} : { reason: event.data.reason }, time: event.time });
269
+ break;
270
+ case 'room/question':
271
+ questions.set(event.data.id, { id: event.data.id, member: event.data.member, questions: event.data.questions, time: event.time });
272
+ break;
273
+ case 'room/question-resolved':
274
+ questions.delete(event.data.id);
275
+ break;
276
+ case 'room/approval-resolved':
277
+ approvals.delete(event.data.id);
278
+ break;
241
279
  case 'room/relay-resolved': {
242
280
  const relay = relayById.get(event.data.id);
243
281
  if (relay === undefined)
@@ -284,12 +322,26 @@ export function replay(events) {
284
322
  const edited = {
285
323
  ...base,
286
324
  ...event.data.title === undefined ? {} : { title: event.data.title },
325
+ ...event.data.member === undefined ? {} : { member: event.data.member },
287
326
  updatedAt: event.time,
288
327
  };
289
328
  taskById.set(edited.id, edited);
290
329
  tasks[tasks.indexOf(task)] = edited;
291
330
  break;
292
331
  }
332
+ case 'room/settings': {
333
+ settings = {
334
+ memberMessages: event.data.memberMessages ?? settings.memberMessages,
335
+ hopLimit: event.data.hopLimit ?? settings.hopLimit,
336
+ paused: event.data.paused ?? settings.paused,
337
+ reportTiming: event.data.reportTiming ?? settings.reportTiming,
338
+ autoTaskFiling: event.data.autoTaskFiling ?? settings.autoTaskFiling,
339
+ reportThreshold: event.data.reportThreshold ?? settings.reportThreshold,
340
+ reportWaitMinutes: event.data.reportWaitMinutes ?? settings.reportWaitMinutes,
341
+ helpReminderMinutes: event.data.helpReminderMinutes ?? settings.helpReminderMinutes,
342
+ };
343
+ break;
344
+ }
293
345
  case 'room/goal': {
294
346
  // Latest goal event wins; a blank text clears the goal.
295
347
  const text = event.data.text.trim();
@@ -337,7 +389,14 @@ export function replay(events) {
337
389
  const planEvent = events.filter(event => event.type === 'room/plan-state').at(-1);
338
390
  const plan = planEvent?.type === 'room/plan-state' ? (({ requests: _requests, ...view }) => view)(planEvent.data) : undefined;
339
391
  const deliveries = replayDeliveries(events);
340
- return { members, relays, tasks, ...executions.size === 0 ? {} : { executions: [...executions.values()] }, ...plan === undefined ? {} : { plan }, ...deliveries.length === 0 ? {} : { deliveries }, runs: [...runs.values()], ...coordinator === undefined ? {} : { coordinator }, ...goal === undefined ? {} : { goal } };
392
+ const jobs = deliveries.length === 0 ? [] : replayJobs(events, deliveries);
393
+ const helps = replayHelps(events);
394
+ const links = replayLinks(events);
395
+ return {
396
+ ...helps.length === 0 ? {} : { helps },
397
+ ...links.linked.length === 0 ? {} : { linked: links.linked },
398
+ ...links.nudgesDismissed.length === 0 ? {} : { helpNudgesDismissed: links.nudgesDismissed }, members, relays, ...approvals.size === 0 ? {} : { approvals: [...approvals.values()] }, ...questions.size === 0 ? {} : { questions: [...questions.values()] }, tasks, ...executions.size === 0 ? {} : { executions: [...executions.values()] }, ...plan === undefined ? {} : { plan }, ...deliveries.length === 0 ? {} : { deliveries }, runs: [...runs.values()], ...coordinator === undefined ? {} : { coordinator }, ...goal === undefined ? {} : { goal }, ...latestReplySeq === undefined ? {} : { latestReplySeq }, ...latestSpeechSeq === undefined ? {} : { latestSpeechSeq }, settings, ...jobs.length === 0 ? {} : { jobs }
399
+ };
341
400
  }
342
401
  /**
343
402
  * The goal progress pair: done tasks over the COUNTABLE total — cancelled
@@ -371,32 +430,50 @@ export function previousCursor(events, member, beforeSeq) {
371
430
  return cursor;
372
431
  }
373
432
  /**
374
- * Whether the roster a member was last told about is stale: any roster-visible
375
- * change with a seq past their dispatch cursor — a member-added or
376
- * member-removed, or a member-updated that touches what the roster section
377
- * shows (instructions = the one-line role, rename = the addressing name). A
378
- * childSessionId/cwd/model-only update is NOT roster-visible (the delegation
379
- * handle is journaled right after a first run; counting it would re-send the
380
- * roster on every member's next dispatch for nothing — the model intent never
381
- * enters any prompt either). A member with no cursor has
382
- * never seen the roster, so their first dispatch always counts as stale.
433
+ * What changed in the room since a member's last dispatch, as one-line
434
+ * notes for the next prompt: other members joining or leaving, renames, role
435
+ * changes, the member's own role change, and a goal change. The full roster
436
+ * rides only the first dispatch's briefing; later dispatches carry these
437
+ * deltas instead (the member's own session holds the rest), and a member who
438
+ * wants the whole picture calls `room_read`. Model, cwd and delegation-handle
439
+ * updates never reach a prompt. A member with no cursor gets no deltas — the
440
+ * briefing already covers everything.
383
441
  * @param events - the session's event log.
384
442
  * @param cursor - the member's pre-dispatch cursor ({@link previousCursor}).
385
- * @returns true when the next dispatch must carry the roster section.
443
+ * @param self - the member's CURRENT name (its own rename is not news to it).
444
+ * @returns the change notes (empty when nothing changed).
386
445
  */
387
- export function rosterStaleSince(events, cursor) {
446
+ export function roomChangesSince(events, cursor, self) {
388
447
  if (cursor === undefined)
389
- return true;
448
+ return [];
449
+ const notes = [];
450
+ let goal;
390
451
  for (const event of events) {
391
452
  if (event.seq <= cursor)
392
453
  continue;
393
- if (event.type === 'room/member-added' || event.type === 'room/member-removed')
394
- return true;
395
- if (event.type === 'room/member-updated'
396
- && (event.data.instructions !== undefined || event.data.rename !== undefined))
397
- return true;
454
+ if (event.type === 'room/member-added' && event.data.name !== self && event.data.kind !== 'main-agent') {
455
+ const detail = [event.data.role, event.data.provider].filter(Boolean).join(' · ');
456
+ notes.push(`${event.data.name} 加入${detail === '' ? '' : `(${detail})`}`);
457
+ }
458
+ else if (event.type === 'room/member-removed' && event.data.name !== self) {
459
+ notes.push(`${event.data.name} 离开`);
460
+ }
461
+ else if (event.type === 'room/member-updated') {
462
+ if (event.data.rename !== undefined && event.data.rename !== self)
463
+ notes.push(`${event.data.name} 改名为 ${event.data.rename}`);
464
+ if (event.data.role !== undefined) {
465
+ const who = event.data.rename ?? event.data.name;
466
+ const subject = who === self ? '你的职责' : `${who} 的职责`;
467
+ notes.push(event.data.role === null ? `${subject}已清除` : `${subject}改为:${event.data.role}`);
468
+ }
469
+ }
470
+ else if (event.type === 'room/goal') {
471
+ goal = { text: event.data.text.trim() };
472
+ }
398
473
  }
399
- return false;
474
+ if (goal !== undefined)
475
+ notes.push(goal.text === '' ? '房间目标已清除' : `房间目标更新为:${goal.text}`);
476
+ return notes;
400
477
  }
401
478
  /**
402
479
  * The role-instructions carry for the next dispatch to a member: 'initial'
@@ -0,0 +1,65 @@
1
+ import type { Session } from '@deepseek-ai/dsh-session';
2
+ import type { SessionEvent } from '@deepseek-ai/dsh-session/types';
3
+ import type { RoomHeldDispatch, RoomHoldReason, RoomMember, RoomRelay, RoomState } from './types.ts';
4
+ /** `RoomHeldDispatch.task` marker: this send starts a task rooted at the dispatch itself. */
5
+ export declare const SELF_TASK = "@self";
6
+ /** The id members and the UI use for a dispatch. */
7
+ export declare function messageId(seq: number): string;
8
+ /** Parse `m12` / `#m12` / `12` back to a dispatch seq. */
9
+ export declare function parseMessageRef(ref: string | undefined): number | undefined;
10
+ /** The hop a dispatch sits at (legacy dispatches count as a chain start). */
11
+ export declare function dispatchHop(events: readonly SessionEvent[], seq: number): number;
12
+ /**
13
+ * The hop of whatever a member is working on right now: its running
14
+ * delivery's hop, or 0 when it is not inside a room delivery (a human
15
+ * chatting with the room's agent directly) so its sends start a chain.
16
+ */
17
+ export declare function workingHop(events: readonly SessionEvent[], state: RoomState, member: RoomMember): number;
18
+ /**
19
+ * Why a message must wait for the human, if it must. A paused room holds
20
+ * everything agents send; a chain past the limit stops; `confirm` holds
21
+ * member-to-member messages only (never the coordinator's delegations or
22
+ * reports, which the human already asked for).
23
+ */
24
+ export declare function holdReason(state: RoomState, hop: number, origin: RoomHeldDispatch['origin']): RoomHoldReason | undefined;
25
+ /** One outgoing agent message. */
26
+ export interface OutgoingMessage {
27
+ readonly to: RoomMember;
28
+ readonly text: string;
29
+ readonly intent: RoomHeldDispatch;
30
+ /** Sender's display name for a held card (`from`). */
31
+ readonly fromName: string;
32
+ }
33
+ /** What {@link sendOrHold} did. */
34
+ export type SendOutcome = {
35
+ readonly delivered: true;
36
+ readonly seq: number;
37
+ } | {
38
+ readonly delivered: false;
39
+ readonly reason: RoomHoldReason;
40
+ readonly relayId: string;
41
+ };
42
+ /**
43
+ * Journal one agent message: a dispatch or a held relay with its intent. The caller
44
+ * flushes and, on delivery, hands `seq` to the engine.
45
+ * @param session - the room session.
46
+ * @param message - recipient, text and the would-be dispatch.
47
+ * @param skipHold - release path: deliver regardless of the hold rules.
48
+ * @returns the dispatch seq, or the hold.
49
+ */
50
+ export declare function sendOrHold(session: Session, message: OutgoingMessage, skipHold?: boolean): SendOutcome;
51
+ /**
52
+ * The would-be dispatch of a held relay, re-anchored for release: a chain
53
+ * the human lets continue past the hop limit starts a fresh budget.
54
+ */
55
+ export declare function releaseIntent(relay: RoomRelay): RoomHeldDispatch | undefined;
56
+ /**
57
+ * The envelope line a delivered message opens with, or undefined for a
58
+ * dispatch without one (legacy journals). Names resolve against the CURRENT
59
+ * roster, falling back to the last known name of a member who left.
60
+ * @param events - the room log.
61
+ * @param seq - the dispatch being delivered.
62
+ * @returns the envelope line.
63
+ */
64
+ export declare function envelopeLine(events: readonly SessionEvent[], seq: number): string | undefined;
65
+ //# sourceMappingURL=messaging.d.ts.map