@khorsheed/dsh-room 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 (95) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.i18n.yaml +6 -0
  3. package/README.md +48 -0
  4. package/README.zh.md +48 -0
  5. package/cordis.patch.yml +6 -0
  6. package/lib/client.js +11481 -0
  7. package/lib/index.js +3231 -0
  8. package/lib/invariant.js +26 -0
  9. package/lib/journal-CuRnWk66.js +435 -0
  10. package/lib/tool.js +350 -0
  11. package/lib/typert.host.d.ts +3 -0
  12. package/lib/typert.host.js +2615 -0
  13. package/lib/typert.remote-client.d.ts +55 -0
  14. package/lib/typert.remote-client.js +1711 -0
  15. package/lib/types/adapter.d.ts +42 -0
  16. package/lib/types/adapter.js +41 -0
  17. package/lib/types/agent-setup.d.ts +141 -0
  18. package/lib/types/agent-setup.js +145 -0
  19. package/lib/types/client/InviteAgentAction.d.ts +13 -0
  20. package/lib/types/client/InviteAgentAction.js +45 -0
  21. package/lib/types/client/InviteDialog.d.ts +90 -0
  22. package/lib/types/client/InviteDialog.js +216 -0
  23. package/lib/types/client/MemberCard.d.ts +33 -0
  24. package/lib/types/client/MemberCard.js +32 -0
  25. package/lib/types/client/MembersView.d.ts +5 -0
  26. package/lib/types/client/MembersView.js +168 -0
  27. package/lib/types/client/RoomComposer.d.ts +21 -0
  28. package/lib/types/client/RoomComposer.js +211 -0
  29. package/lib/types/client/RoomDockCapsules.d.ts +64 -0
  30. package/lib/types/client/RoomDockCapsules.js +277 -0
  31. package/lib/types/client/RoomEventView.d.ts +11 -0
  32. package/lib/types/client/RoomEventView.js +17 -0
  33. package/lib/types/client/RoomModelPicker.d.ts +18 -0
  34. package/lib/types/client/RoomModelPicker.js +134 -0
  35. package/lib/types/client/RoomPlanView.d.ts +17 -0
  36. package/lib/types/client/RoomPlanView.js +88 -0
  37. package/lib/types/client/RoomQueueStrip.d.ts +26 -0
  38. package/lib/types/client/RoomQueueStrip.js +28 -0
  39. package/lib/types/client/RoomRecoveryView.d.ts +11 -0
  40. package/lib/types/client/RoomRecoveryView.js +38 -0
  41. package/lib/types/client/RoomRelayView.d.ts +12 -0
  42. package/lib/types/client/RoomRelayView.js +20 -0
  43. package/lib/types/client/RoomRunView.d.ts +15 -0
  44. package/lib/types/client/RoomRunView.js +50 -0
  45. package/lib/types/client/RoomSpeechView.d.ts +18 -0
  46. package/lib/types/client/RoomSpeechView.js +62 -0
  47. package/lib/types/client/RoomStatsLine.d.ts +61 -0
  48. package/lib/types/client/RoomStatsLine.js +138 -0
  49. package/lib/types/client/RoomTaskLineView.d.ts +13 -0
  50. package/lib/types/client/RoomTaskLineView.js +21 -0
  51. package/lib/types/client/RoomTodoStrip.d.ts +24 -0
  52. package/lib/types/client/RoomTodoStrip.js +64 -0
  53. package/lib/types/client/format.d.ts +31 -0
  54. package/lib/types/client/format.js +48 -0
  55. package/lib/types/client/icons.d.ts +27 -0
  56. package/lib/types/client/icons.js +11 -0
  57. package/lib/types/client/index.d.ts +39 -0
  58. package/lib/types/client/index.js +471 -0
  59. package/lib/types/client/locales.d.ts +422 -0
  60. package/lib/types/client/locales.js +413 -0
  61. package/lib/types/client/member-color.d.ts +13 -0
  62. package/lib/types/client/member-color.js +22 -0
  63. package/lib/types/client/name-pool.d.ts +17 -0
  64. package/lib/types/client/name-pool.js +25 -0
  65. package/lib/types/client/nodes.d.ts +136 -0
  66. package/lib/types/client/nodes.js +212 -0
  67. package/lib/types/client/preset-visibility.d.ts +91 -0
  68. package/lib/types/client/preset-visibility.js +113 -0
  69. package/lib/types/client/request-ids.d.ts +7 -0
  70. package/lib/types/client/request-ids.js +21 -0
  71. package/lib/types/client/room-store.d.ts +137 -0
  72. package/lib/types/client/room-store.js +244 -0
  73. package/lib/types/client/slots.d.ts +319 -0
  74. package/lib/types/client/slots.js +1 -0
  75. package/lib/types/deliveries.d.ts +5 -0
  76. package/lib/types/deliveries.js +20 -0
  77. package/lib/types/dispatch.d.ts +105 -0
  78. package/lib/types/dispatch.js +467 -0
  79. package/lib/types/index.d.ts +301 -0
  80. package/lib/types/index.js +1200 -0
  81. package/lib/types/invariant.d.ts +16 -0
  82. package/lib/types/invariant.js +24 -0
  83. package/lib/types/journal.d.ts +117 -0
  84. package/lib/types/journal.js +434 -0
  85. package/lib/types/plan-service.d.ts +34 -0
  86. package/lib/types/plan-service.js +266 -0
  87. package/lib/types/plan.d.ts +168 -0
  88. package/lib/types/plan.js +494 -0
  89. package/lib/types/tool.d.ts +56 -0
  90. package/lib/types/tool.js +389 -0
  91. package/lib/types/types.d.ts +813 -0
  92. package/lib/types/types.js +1 -0
  93. package/locale/en.json +6 -0
  94. package/locale/zh.json +6 -0
  95. package/package.json +131 -0
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Package-owned invariant companion for `@khorsheed/dsh-room`.
3
+ * @module @khorsheed/dsh-room/invariant
4
+ */
5
+ import type { Context } from '@deepseek-ai/cordis';
6
+ /** Cordis companion plugin name. */
7
+ export declare const name = "room-invariant";
8
+ /** Service required before the companion can reserve package ownership. */
9
+ export declare const inject: string[];
10
+ /**
11
+ * Register this package's invariant companion.
12
+ * @param ctx - Cordis context carrying the invariant service.
13
+ * @returns the installed registration's disposer after setup succeeds.
14
+ */
15
+ export declare const apply: (ctx: Context) => Promise<() => void>;
16
+ //# sourceMappingURL=invariant.d.ts.map
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Package-owned invariant companion for `@khorsheed/dsh-room`.
3
+ * @module @khorsheed/dsh-room/invariant
4
+ */
5
+ const PACKAGE_NAME = '@khorsheed/dsh-room';
6
+ /** Cordis companion plugin name. */
7
+ export const name = 'room-invariant';
8
+ /** Service required before the companion can reserve package ownership. */
9
+ export const inject = ['invariants'];
10
+ /**
11
+ * Placeholder (Step 0/1 spike): no runtime invariant yet. The `room/created`
12
+ * marker write passes through Session.append, whose validation rejects any
13
+ * non-JSON payload, and the client contributions ride the slot/locale
14
+ * lifecycles disposed with the plugin fiber. Later steps (roster transition
15
+ * validation, dispatch invariants) install real checks here.
16
+ */
17
+ const install = () => { };
18
+ /**
19
+ * Register this package's invariant companion.
20
+ * @param ctx - Cordis context carrying the invariant service.
21
+ * @returns the installed registration's disposer after setup succeeds.
22
+ */
23
+ export const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
24
+ /* jscpd:ignore-end */
@@ -0,0 +1,117 @@
1
+ import type { SessionEvent } from '@deepseek-ai/dsh-session/types';
2
+ import type { RoomMember, RoomState, RoomTask, RoomTaskProgress } from './types.ts';
3
+ /**
4
+ * The native DSH member's addressing name in new rooms. It starts as the
5
+ * coordinator; bare messages follow the selected coordinator thereafter.
6
+ * Existing journals retain their recorded names, including legacy main.
7
+ */
8
+ export declare const MAIN_AGENT_MEMBER = "dsh";
9
+ /**
10
+ * Every `room/*` session-event type this package introduces. Room registers
11
+ * them into the harness's KNOWN_SESSION_EVENT_TYPES persistence catalog at
12
+ * apply time (see the RoomService constructor): the catalog's read-path
13
+ * refusal exists so a build that does NOT understand an event type never
14
+ * silently mangles the log — a build with room mounted understands these, so
15
+ * registration is the semantically correct declaration, and a build without
16
+ * room keeps refusing them. The catalog's own header defers a registration
17
+ * surface for out-of-repo plugins "until such a consumer exists"; this
18
+ * in-place add is that surface's temporary form — migrate to the official
19
+ * one when it lands upstream.
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'];
22
+ /** Whether an event log carries the room identity marker. */
23
+ export declare function isRoomLog(events: readonly SessionEvent[]): boolean;
24
+ /**
25
+ * Parse the leading `@name` tokens of a raw composer message.
26
+ * @param raw - the raw text.
27
+ * @returns the addressed names (deduped, order-preserving) and the body with
28
+ * the mention prefix stripped. Only LEADING tokens address: an `@name`
29
+ * inside prose is plain text (a bare message goes to the selected coordinator).
30
+ */
31
+ export declare function parseMentions(raw: string): {
32
+ readonly targets: readonly string[];
33
+ readonly text: string;
34
+ };
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
+ /**
50
+ * Fold a session's event log into the room state.
51
+ *
52
+ * Write-side invariants (invite/update/remove validate before appending) are
53
+ * NOT re-enforced here: a replayed log may be hand-edited or forked, so the
54
+ * fold is defensive — a duplicate member-added keeps the first record (first
55
+ * wins, a replay cannot re-seat a member), and updates/removals naming an
56
+ * unknown member are dropped. Unknown event types (e.g. a `room/note` from
57
+ * the dropped blackboard design) fall through the switch and are skipped.
58
+ * Removal also drops the member's run state: the roster reads clean after a
59
+ * remove. Relays and tasks fold by id, latest resolution winning.
60
+ * @param events - the session's event log.
61
+ * @returns the folded room state (empty for a non-room log).
62
+ */
63
+ export declare function replay(events: readonly SessionEvent[]): RoomState;
64
+ /**
65
+ * The goal progress pair: done tasks over the COUNTABLE total — cancelled
66
+ * and failed tasks leave the denominator (a cancelled task was never part
67
+ * of the plan; a failed one was planned but never advanced it).
68
+ * @param tasks - the folded task board.
69
+ * @returns done/total.
70
+ */
71
+ export declare function taskProgress(tasks: readonly RoomTask[]): RoomTaskProgress;
72
+ /**
73
+ * The member's dispatch cursor as of JUST BEFORE a given dispatch event: the
74
+ * latest earlier dispatch naming them. The engine reads the role-instruction
75
+ * carry against this cursor (pass {@link Number.MAX_SAFE_INTEGER} for a
76
+ * dispatch that is not itself journaled, e.g. a relay delivery).
77
+ * @param events - the session's event log.
78
+ * @param member - the dispatch target.
79
+ * @param beforeSeq - the current dispatch event's seq.
80
+ * @returns the previous dispatch seq, or undefined on the member's first dispatch.
81
+ */
82
+ export declare function previousCursor(events: readonly SessionEvent[], member: string, beforeSeq: number): number | undefined;
83
+ /**
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.
93
+ * @param events - the session's event log.
94
+ * @param cursor - the member's pre-dispatch cursor ({@link previousCursor}).
95
+ * @returns true when the next dispatch must carry the roster section.
96
+ */
97
+ export declare function rosterStaleSince(events: readonly SessionEvent[], cursor: number | undefined): boolean;
98
+ /**
99
+ * The role-instructions carry for the next dispatch to a member: 'initial'
100
+ * when the member has never been dispatched (their instructions open the
101
+ * first prompt), 'update' when the latest member-added/member-updated event
102
+ * naming them postdates their dispatch cursor (an edit rides the next
103
+ * dispatch as a context update). Instructions events older than the cursor
104
+ * were already carried.
105
+ * @param events - the session's event log.
106
+ * @param member - the dispatch target.
107
+ * @param cursor - the member's pre-dispatch cursor ({@link previousCursor}).
108
+ * @returns the carry, or undefined when nothing needs carrying.
109
+ */
110
+ export declare function pendingInstructions(events: readonly SessionEvent[], member: string, cursor: number | undefined): {
111
+ readonly kind: 'initial' | 'update';
112
+ readonly instructions: string;
113
+ } | undefined;
114
+ /** Resolve legacy join identity through renames; never key a new dispatch by display name. */
115
+ export declare function memberId(events: readonly SessionEvent[], member: RoomMember): string;
116
+ export declare function coordinatorMember(state: RoomState, events?: readonly SessionEvent[]): RoomMember | undefined;
117
+ //# sourceMappingURL=journal.d.ts.map
@@ -0,0 +1,434 @@
1
+ /**
2
+ * Room journal replay: folds the room session's `room/*` custom events into
3
+ * the derived {@link RoomState} — roster, notification relays, the task
4
+ * board, and run states. Pure: the Remote surface reads through it, the
5
+ * dispatch engine reads instruction cursors through it, and the unit tests
6
+ * exercise it without a cordis composition. Reload-replay recovers the exact
7
+ * same state, which is what makes the journal the room's single source of
8
+ * truth. There is NO blackboard fold: member prompts never consume the
9
+ * room's running log (see the design note) — speech/dispatch events are
10
+ * journaled for UI projection and replay only.
11
+ * @module @khorsheed/dsh-room/journal
12
+ */
13
+ import { replayDeliveries } from './deliveries.js';
14
+ /**
15
+ * The native DSH member's addressing name in new rooms. It starts as the
16
+ * coordinator; bare messages follow the selected coordinator thereafter.
17
+ * Existing journals retain their recorded names, including legacy main.
18
+ */
19
+ export const MAIN_AGENT_MEMBER = 'dsh';
20
+ /**
21
+ * Every `room/*` session-event type this package introduces. Room registers
22
+ * them into the harness's KNOWN_SESSION_EVENT_TYPES persistence catalog at
23
+ * apply time (see the RoomService constructor): the catalog's read-path
24
+ * refusal exists so a build that does NOT understand an event type never
25
+ * silently mangles the log — a build with room mounted understands these, so
26
+ * registration is the semantically correct declaration, and a build without
27
+ * room keeps refusing them. The catalog's own header defers a registration
28
+ * surface for out-of-repo plugins "until such a consumer exists"; this
29
+ * in-place add is that surface's temporary form — migrate to the official
30
+ * one when it lands upstream.
31
+ */
32
+ export const ROOM_EVENT_TYPES = [
33
+ 'room/created',
34
+ 'room/plan-state',
35
+ 'room/coordinator',
36
+ 'room/delivery-state',
37
+ 'room/member-added',
38
+ 'room/member-updated',
39
+ 'room/member-removed',
40
+ 'room/dispatch',
41
+ 'room/speech',
42
+ 'room/run-state',
43
+ 'room/relay',
44
+ 'room/relay-resolved',
45
+ 'room/task-added',
46
+ 'room/task-updated',
47
+ 'room/task-edited',
48
+ 'room/goal',
49
+ ];
50
+ /** Whether an event log carries the room identity marker. */
51
+ export function isRoomLog(events) {
52
+ return events.some(event => event.type === 'room/created');
53
+ }
54
+ /**
55
+ * Parse the leading `@name` tokens of a raw composer message.
56
+ * @param raw - the raw text.
57
+ * @returns the addressed names (deduped, order-preserving) and the body with
58
+ * the mention prefix stripped. Only LEADING tokens address: an `@name`
59
+ * inside prose is plain text (a bare message goes to the selected coordinator).
60
+ */
61
+ export function parseMentions(raw) {
62
+ const targets = [];
63
+ let rest = raw.trim();
64
+ for (;;) {
65
+ const match = /^@(\S+)(?:\s+|$)/.exec(rest);
66
+ if (match === null)
67
+ break;
68
+ const name = match[1];
69
+ if (!targets.includes(name))
70
+ targets.push(name);
71
+ rest = rest.slice(match[0].length).trim();
72
+ }
73
+ return { targets, text: rest };
74
+ }
75
+ /**
76
+ * The fallback notification channel: a member reply's TRAILING own line of
77
+ * the form `@name <content>` (the format the roster injection teaches).
78
+ * Only the last line counts — an `@name` inside prose is a mention, not a
79
+ * notification. Leading whitespace on that line is tolerated (models indent
80
+ * under their own bullet/list style). The caller checks the target against
81
+ * the roster.
82
+ * @param text - the member's reply text.
83
+ * @returns the parsed directive, or undefined.
84
+ */
85
+ export function parseRelayDirective(text) {
86
+ const trimmed = text.trimEnd();
87
+ if (trimmed === '')
88
+ return undefined;
89
+ const last = trimmed.slice(trimmed.lastIndexOf('\n') + 1).trimStart();
90
+ const match = /^@(\S+)\s+(\S[\s\S]*)$/.exec(last);
91
+ if (match === null)
92
+ return undefined;
93
+ return { to: match[1], content: match[2].trim() };
94
+ }
95
+ /**
96
+ * Fold a session's event log into the room state.
97
+ *
98
+ * Write-side invariants (invite/update/remove validate before appending) are
99
+ * NOT re-enforced here: a replayed log may be hand-edited or forked, so the
100
+ * fold is defensive — a duplicate member-added keeps the first record (first
101
+ * wins, a replay cannot re-seat a member), and updates/removals naming an
102
+ * unknown member are dropped. Unknown event types (e.g. a `room/note` from
103
+ * the dropped blackboard design) fall through the switch and are skipped.
104
+ * Removal also drops the member's run state: the roster reads clean after a
105
+ * remove. Relays and tasks fold by id, latest resolution winning.
106
+ * @param events - the session's event log.
107
+ * @returns the folded room state (empty for a non-room log).
108
+ */
109
+ export function replay(events) {
110
+ const members = [];
111
+ const byName = new Map();
112
+ const relays = [];
113
+ const relayById = new Map();
114
+ const tasks = [];
115
+ const taskById = new Map();
116
+ const runs = new Map();
117
+ let goal;
118
+ let coordinator;
119
+ for (const event of events) {
120
+ switch (event.type) {
121
+ case 'room/coordinator': {
122
+ if (event.data.revision > (coordinator?.revision ?? 0))
123
+ coordinator = event.data;
124
+ break;
125
+ }
126
+ case 'room/member-added': {
127
+ if (byName.has(event.data.name))
128
+ break;
129
+ const added = {
130
+ id: event.data.id ?? `legacy:${event.seq}`,
131
+ name: event.data.name,
132
+ kind: event.data.kind,
133
+ invitedBy: event.data.invitedBy,
134
+ ...event.data.provider === undefined ? {} : { provider: event.data.provider },
135
+ ...event.data.instructions === undefined ? {} : { instructions: event.data.instructions },
136
+ ...event.data.cwd === undefined ? {} : { cwd: event.data.cwd },
137
+ ...event.data.model === undefined ? {} : { model: event.data.model },
138
+ ...event.data.childSessionId === undefined ? {} : { childSessionId: event.data.childSessionId },
139
+ };
140
+ byName.set(added.name, added);
141
+ members.push(added);
142
+ break;
143
+ }
144
+ case 'room/member-updated': {
145
+ const member = byName.get(event.data.name);
146
+ if (member === undefined)
147
+ break;
148
+ // null CLEARS a field (rebuild without the key —
149
+ // exactOptionalPropertyTypes forbids writing undefined into it).
150
+ const withoutInstructions = event.data.instructions === null
151
+ ? (({ instructions: _cleared, ...rest }) => rest)(member)
152
+ : { ...member, ...event.data.instructions === undefined ? {} : { instructions: event.data.instructions } };
153
+ const withoutCwd = event.data.cwd === null
154
+ ? (({ cwd: _cleared, ...rest }) => rest)(withoutInstructions)
155
+ : { ...withoutInstructions, ...event.data.cwd === undefined ? {} : { cwd: event.data.cwd } };
156
+ const withoutModel = event.data.model === null
157
+ ? (({ model: _cleared, ...rest }) => rest)(withoutCwd)
158
+ : { ...withoutCwd, ...event.data.model === undefined ? {} : { model: event.data.model } };
159
+ const patched = {
160
+ ...withoutModel,
161
+ ...event.data.childSessionId === undefined ? {} : { childSessionId: event.data.childSessionId },
162
+ };
163
+ // A rename migrates every name-keyed projection: the roster key, the
164
+ // tasks' member (and blockedBy), the relays' from/to, and the runs
165
+ // key. The child session id rides the member record and stays. A
166
+ // colliding rename (the new name is taken — a hand-edited log; the
167
+ // write path validates) drops the rename, keeps the rest.
168
+ const rename = event.data.rename;
169
+ if (rename === undefined || rename === member.name || byName.has(rename)) {
170
+ byName.set(member.name, patched);
171
+ members[members.indexOf(member)] = patched;
172
+ break;
173
+ }
174
+ const renamed = { ...patched, name: rename };
175
+ byName.delete(member.name);
176
+ byName.set(rename, renamed);
177
+ members[members.indexOf(member)] = renamed;
178
+ for (let index = 0; index < tasks.length; index++) {
179
+ const task = tasks[index];
180
+ if (task.member !== member.name && task.blockedBy !== member.name)
181
+ continue;
182
+ const migrated = {
183
+ ...task,
184
+ ...task.member === member.name ? { member: rename } : {},
185
+ ...task.blockedBy === member.name ? { blockedBy: rename } : {},
186
+ };
187
+ taskById.set(migrated.id, migrated);
188
+ tasks[index] = migrated;
189
+ }
190
+ for (let index = 0; index < relays.length; index++) {
191
+ const relay = relays[index];
192
+ if (relay.from !== member.name && relay.to !== member.name)
193
+ continue;
194
+ const migrated = {
195
+ ...relay,
196
+ from: relay.from === member.name ? rename : relay.from,
197
+ to: relay.to === member.name ? rename : relay.to,
198
+ };
199
+ relayById.set(migrated.id, migrated);
200
+ relays[index] = migrated;
201
+ }
202
+ const run = runs.get(member.name);
203
+ if (run !== undefined) {
204
+ runs.delete(member.name);
205
+ runs.set(rename, { ...run, member: rename });
206
+ }
207
+ break;
208
+ }
209
+ case 'room/member-removed': {
210
+ const member = byName.get(event.data.name);
211
+ if (member === undefined)
212
+ break;
213
+ byName.delete(event.data.name);
214
+ members.splice(members.indexOf(member), 1);
215
+ runs.delete(event.data.name);
216
+ break;
217
+ }
218
+ case 'room/relay': {
219
+ if (relayById.has(event.data.id))
220
+ break;
221
+ const relay = {
222
+ id: event.data.id,
223
+ from: event.data.from,
224
+ to: event.data.to,
225
+ content: event.data.content,
226
+ state: 'pending',
227
+ ...event.data.provenance === undefined ? {} : { provenance: event.data.provenance },
228
+ };
229
+ relayById.set(relay.id, relay);
230
+ relays.push(relay);
231
+ break;
232
+ }
233
+ case 'room/relay-resolved': {
234
+ const relay = relayById.get(event.data.id);
235
+ if (relay === undefined)
236
+ break;
237
+ const resolved = { ...relay, state: event.data.state };
238
+ relayById.set(resolved.id, resolved);
239
+ relays[relays.indexOf(relay)] = resolved;
240
+ break;
241
+ }
242
+ case 'room/task-added': {
243
+ if (taskById.has(event.data.id))
244
+ break;
245
+ const task = {
246
+ id: event.data.id,
247
+ member: event.data.member,
248
+ title: event.data.title,
249
+ ...event.data.deliveryId === undefined ? {} : { deliveryId: event.data.deliveryId },
250
+ status: event.data.status,
251
+ ...event.data.blockedBy === undefined ? {} : { blockedBy: event.data.blockedBy },
252
+ updatedAt: event.time,
253
+ };
254
+ taskById.set(task.id, task);
255
+ tasks.push(task);
256
+ break;
257
+ }
258
+ case 'room/task-updated': {
259
+ const task = taskById.get(event.data.id);
260
+ if (task === undefined)
261
+ break;
262
+ const updated = { ...task, status: event.data.status, updatedAt: event.time };
263
+ taskById.set(updated.id, updated);
264
+ tasks[tasks.indexOf(task)] = updated;
265
+ break;
266
+ }
267
+ case 'room/task-edited': {
268
+ const task = taskById.get(event.data.id);
269
+ if (task === undefined)
270
+ break;
271
+ // An explicit null blockedBy CLEARS the wait: rebuild without the key
272
+ // (exactOptionalPropertyTypes forbids writing undefined into it).
273
+ const base = event.data.blockedBy === null
274
+ ? (({ blockedBy: _cleared, ...rest }) => rest)(task)
275
+ : { ...task, ...event.data.blockedBy === undefined ? {} : { blockedBy: event.data.blockedBy } };
276
+ const edited = {
277
+ ...base,
278
+ ...event.data.title === undefined ? {} : { title: event.data.title },
279
+ updatedAt: event.time,
280
+ };
281
+ taskById.set(edited.id, edited);
282
+ tasks[tasks.indexOf(task)] = edited;
283
+ break;
284
+ }
285
+ case 'room/goal': {
286
+ // Latest goal event wins; a blank text clears the goal.
287
+ const text = event.data.text.trim();
288
+ goal = text === '' ? undefined : text;
289
+ break;
290
+ }
291
+ case 'room/run-state': {
292
+ const current = runs.get(event.data.member);
293
+ // An older turn may finish journaling after core admits its successor.
294
+ // Its delivery outcome remains durable, but it cannot replace that successor.
295
+ if (event.data.state !== 'running' && event.data.runId !== undefined && current?.runId !== undefined && current.runId !== event.data.runId)
296
+ break;
297
+ runs.set(event.data.member, {
298
+ member: event.data.member,
299
+ state: event.data.state,
300
+ startedAt: event.data.startedAt,
301
+ ...event.data.runId === undefined ? {} : { runId: event.data.runId },
302
+ ...event.data.elapsedMs === undefined ? {} : { elapsedMs: event.data.elapsedMs },
303
+ ...event.data.error === undefined ? {} : { error: event.data.error },
304
+ });
305
+ break;
306
+ }
307
+ default:
308
+ break;
309
+ }
310
+ }
311
+ const planEvent = events.filter(event => event.type === 'room/plan-state').at(-1);
312
+ const plan = planEvent?.type === 'room/plan-state' ? (({ requests: _requests, ...view }) => view)(planEvent.data) : undefined;
313
+ const deliveries = replayDeliveries(events);
314
+ return { members, relays, tasks, ...plan === undefined ? {} : { plan }, ...deliveries.length === 0 ? {} : { deliveries }, runs: [...runs.values()], ...coordinator === undefined ? {} : { coordinator }, ...goal === undefined ? {} : { goal } };
315
+ }
316
+ /**
317
+ * The goal progress pair: done tasks over the COUNTABLE total — cancelled
318
+ * and failed tasks leave the denominator (a cancelled task was never part
319
+ * of the plan; a failed one was planned but never advanced it).
320
+ * @param tasks - the folded task board.
321
+ * @returns done/total.
322
+ */
323
+ export function taskProgress(tasks) {
324
+ const countable = tasks.filter(task => task.status !== 'cancelled' && task.status !== 'failed');
325
+ return { done: countable.filter(task => task.status === 'done').length, total: countable.length };
326
+ }
327
+ /**
328
+ * The member's dispatch cursor as of JUST BEFORE a given dispatch event: the
329
+ * latest earlier dispatch naming them. The engine reads the role-instruction
330
+ * carry against this cursor (pass {@link Number.MAX_SAFE_INTEGER} for a
331
+ * dispatch that is not itself journaled, e.g. a relay delivery).
332
+ * @param events - the session's event log.
333
+ * @param member - the dispatch target.
334
+ * @param beforeSeq - the current dispatch event's seq.
335
+ * @returns the previous dispatch seq, or undefined on the member's first dispatch.
336
+ */
337
+ export function previousCursor(events, member, beforeSeq) {
338
+ let cursor;
339
+ for (const event of events) {
340
+ if (event.type !== 'room/dispatch' || event.seq >= beforeSeq)
341
+ continue;
342
+ if (event.data.targets.includes(member))
343
+ cursor = event.seq;
344
+ }
345
+ return cursor;
346
+ }
347
+ /**
348
+ * Whether the roster a member was last told about is stale: any roster-visible
349
+ * change with a seq past their dispatch cursor — a member-added or
350
+ * member-removed, or a member-updated that touches what the roster section
351
+ * shows (instructions = the one-line role, rename = the addressing name). A
352
+ * childSessionId/cwd/model-only update is NOT roster-visible (the delegation
353
+ * handle is journaled right after a first run; counting it would re-send the
354
+ * roster on every member's next dispatch for nothing — the model intent never
355
+ * enters any prompt either). A member with no cursor has
356
+ * never seen the roster, so their first dispatch always counts as stale.
357
+ * @param events - the session's event log.
358
+ * @param cursor - the member's pre-dispatch cursor ({@link previousCursor}).
359
+ * @returns true when the next dispatch must carry the roster section.
360
+ */
361
+ export function rosterStaleSince(events, cursor) {
362
+ if (cursor === undefined)
363
+ return true;
364
+ for (const event of events) {
365
+ if (event.seq <= cursor)
366
+ continue;
367
+ if (event.type === 'room/member-added' || event.type === 'room/member-removed')
368
+ return true;
369
+ if (event.type === 'room/member-updated'
370
+ && (event.data.instructions !== undefined || event.data.rename !== undefined))
371
+ return true;
372
+ }
373
+ return false;
374
+ }
375
+ /**
376
+ * The role-instructions carry for the next dispatch to a member: 'initial'
377
+ * when the member has never been dispatched (their instructions open the
378
+ * first prompt), 'update' when the latest member-added/member-updated event
379
+ * naming them postdates their dispatch cursor (an edit rides the next
380
+ * dispatch as a context update). Instructions events older than the cursor
381
+ * were already carried.
382
+ * @param events - the session's event log.
383
+ * @param member - the dispatch target.
384
+ * @param cursor - the member's pre-dispatch cursor ({@link previousCursor}).
385
+ * @returns the carry, or undefined when nothing needs carrying.
386
+ */
387
+ export function pendingInstructions(events, member, cursor) {
388
+ let lastSeq;
389
+ let lastInstructions;
390
+ for (const event of events) {
391
+ if (event.type === 'room/member-added' && event.data.name === member
392
+ && event.data.instructions !== undefined) {
393
+ lastSeq = event.seq;
394
+ lastInstructions = event.data.instructions;
395
+ }
396
+ else if (event.type === 'room/member-updated' && event.data.name === member) {
397
+ // A null instructions CLEARS the preset: nothing rides the next dispatch.
398
+ if (event.data.instructions === null) {
399
+ lastSeq = undefined;
400
+ lastInstructions = undefined;
401
+ }
402
+ else if (event.data.instructions !== undefined) {
403
+ lastSeq = event.seq;
404
+ lastInstructions = event.data.instructions;
405
+ }
406
+ }
407
+ }
408
+ if (lastSeq === undefined || lastInstructions === undefined)
409
+ return undefined;
410
+ if (cursor === undefined)
411
+ return { kind: 'initial', instructions: lastInstructions };
412
+ if (lastSeq > cursor)
413
+ return { kind: 'update', instructions: lastInstructions };
414
+ return undefined;
415
+ }
416
+ /** Resolve legacy join identity through renames; never key a new dispatch by display name. */
417
+ export function memberId(events, member) {
418
+ if (member.id !== undefined)
419
+ return member.id;
420
+ let name = member.name;
421
+ for (let index = events.length - 1; index >= 0; index--) {
422
+ const event = events[index];
423
+ if (event.type === 'room/member-updated' && event.data.rename === name)
424
+ name = event.data.name;
425
+ if (event.type === 'room/member-added' && event.data.name === name)
426
+ return event.data.id ?? `legacy:${event.seq}`;
427
+ }
428
+ throw new Error('Room member has no join identity');
429
+ }
430
+ export function coordinatorMember(state, events) {
431
+ if (state.coordinator === undefined)
432
+ return state.members.find(member => member.kind === 'main-agent');
433
+ return state.members.find(member => (member.id ?? (events === undefined ? undefined : memberId(events, member))) === state.coordinator.memberId);
434
+ }
@@ -0,0 +1,34 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import type { Session } from '@deepseek-ai/dsh-session';
3
+ import type { PlanActor, RoomPlan } from './plan.ts';
4
+ import type { DispatchEngine } from './dispatch.ts';
5
+ export declare function readPlan(room: Session): RoomPlan | undefined;
6
+ /** One serialized writer for durable goal transitions and automatic leaf dispatch. */
7
+ export declare class PlanService {
8
+ private readonly ctx;
9
+ private readonly engine;
10
+ private readonly tails;
11
+ private readonly recovered;
12
+ private readonly timers;
13
+ constructor(ctx: Context, engine: () => DispatchEngine);
14
+ private locked;
15
+ /** Cold inspection projects the restart pause without writing or starting agents. */
16
+ view(sessionId: string, view: Omit<RoomPlan, 'requests'>): Omit<RoomPlan, 'requests'>;
17
+ recover(room: Session): Promise<void>;
18
+ private recoverUnlocked;
19
+ private write;
20
+ private armDeadline;
21
+ command(room: Session, json: string, actor: PlanActor): Promise<RoomPlan>;
22
+ allows(room: Session, dispatchSeq?: number): boolean;
23
+ admitted(room: Session, dispatchSeq?: number): Promise<void>;
24
+ settled(room: Session, dispatchSeq: number | undefined, outcome: {
25
+ state: 'done' | 'failed' | 'cancelled';
26
+ text?: string;
27
+ error?: string;
28
+ }): Promise<void>;
29
+ /** Human goal reconciliation settles its room delivery and supersedes stale queued reports. */
30
+ private reconcileDelivery;
31
+ private pump;
32
+ dispose(): void;
33
+ }
34
+ //# sourceMappingURL=plan-service.d.ts.map