@lumenflow/cli 5.1.0 → 5.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 (61) hide show
  1. package/dist/capacity-snapshot-emitter.js +10 -12
  2. package/dist/capacity-snapshot-emitter.js.map +1 -1
  3. package/dist/docs-generate-pack-reference.js +54 -0
  4. package/dist/docs-generate-pack-reference.js.map +1 -1
  5. package/dist/flow-bottlenecks.js +55 -0
  6. package/dist/flow-bottlenecks.js.map +1 -1
  7. package/dist/kernel-event-sync/lifecycle-emitters.js +13 -14
  8. package/dist/kernel-event-sync/lifecycle-emitters.js.map +1 -1
  9. package/dist/kernel-event-sync/narrow-emissions.js +7 -4
  10. package/dist/kernel-event-sync/narrow-emissions.js.map +1 -1
  11. package/dist/kernel-event-sync/software-delivery-emitters.js +91 -0
  12. package/dist/kernel-event-sync/software-delivery-emitters.js.map +1 -1
  13. package/dist/lumenflow-upgrade.js +39 -10
  14. package/dist/lumenflow-upgrade.js.map +1 -1
  15. package/dist/metrics-snapshot.js +51 -0
  16. package/dist/metrics-snapshot.js.map +1 -1
  17. package/dist/orchestrate-init-status.js +24 -11
  18. package/dist/orchestrate-init-status.js.map +1 -1
  19. package/dist/pack-install.js +27 -20
  20. package/dist/pack-install.js.map +1 -1
  21. package/dist/pack-publish.js +34 -26
  22. package/dist/pack-publish.js.map +1 -1
  23. package/dist/release.js +308 -149
  24. package/dist/release.js.map +1 -1
  25. package/dist/strict-progress.js +49 -0
  26. package/dist/strict-progress.js.map +1 -1
  27. package/dist/temp-dir-cleanup.js +112 -0
  28. package/dist/temp-dir-cleanup.js.map +1 -0
  29. package/dist/validate-agent-sync.js +6 -5
  30. package/dist/validate-agent-sync.js.map +1 -1
  31. package/dist/wu-escalate.js +33 -0
  32. package/dist/wu-escalate.js.map +1 -1
  33. package/dist/wu-preflight.js +28 -0
  34. package/dist/wu-preflight.js.map +1 -1
  35. package/dist/wu-recover.js +35 -0
  36. package/dist/wu-recover.js.map +1 -1
  37. package/dist/wu-spawn-strategy-resolver.js +4 -0
  38. package/dist/wu-spawn-strategy-resolver.js.map +1 -1
  39. package/dist/wu-validate.js +63 -0
  40. package/dist/wu-validate.js.map +1 -1
  41. package/package.json +11 -11
  42. package/packs/agent-runtime/manifest.ts +55 -4
  43. package/packs/agent-runtime/manifest.yaml +16 -6
  44. package/packs/agent-runtime/orchestration.ts +26 -1
  45. package/packs/agent-runtime/package.json +1 -1
  46. package/packs/agent-runtime/remote-controls/operations.ts +6 -0
  47. package/packs/agent-runtime/tool-impl/remote-controls.mock.ts +4 -0
  48. package/packs/agent-runtime/turn-lifecycle-events.ts +90 -1
  49. package/packs/sidekick/manifest.yaml +6 -0
  50. package/packs/sidekick/package.json +2 -1
  51. package/packs/sidekick/sidekick-events.ts +195 -18
  52. package/packs/sidekick/src/adapters/control-plane-bridge.adapter.ts +18 -10
  53. package/packs/sidekick/src/adapters/filesystem-bridge.adapter.ts +4 -0
  54. package/packs/sidekick/src/domain/channel.types.ts +34 -54
  55. package/packs/sidekick/src/ports/channel-bridge.port.ts +29 -12
  56. package/packs/sidekick/tool-impl/channel-tools.ts +47 -16
  57. package/packs/sidekick/tool-impl/system-tools.ts +4 -6
  58. package/packs/software-delivery/manifest.ts +94 -7
  59. package/packs/software-delivery/manifest.yaml +42 -5
  60. package/packs/software-delivery/package.json +1 -1
  61. package/packs/software-delivery/tool-impl/wu-lifecycle-tools.ts +30 -0
@@ -17,6 +17,9 @@ import {
17
17
  AGENT_RUNTIME_AGENT_WORKFLOW_NODE_ID_METADATA_KEY,
18
18
  } from './constants.js';
19
19
  import {
20
+ AGENT_RUNTIME_BUDGET_LEVELS,
21
+ AGENT_RUNTIME_BUDGET_SCOPES,
22
+ AGENT_RUNTIME_TURN_COMPLETED_STATUSES,
20
23
  buildApprovalRequiredEvent,
21
24
  buildBudgetThresholdEvent,
22
25
  buildScheduledWakeupSetEvent,
@@ -1017,7 +1020,12 @@ async function runGovernedAgentLoopInternal(
1017
1020
  buildTurnCompletedEvent({
1018
1021
  session_id: sessionId,
1019
1022
  turn_index: currentTurnIndex,
1020
- status: normalizedTurn.status,
1023
+ // WU-2829: TurnCompletedEvent is only emitted after execute-turn
1024
+ // returns a normalized, successful turn output — every reachable
1025
+ // emission here represents a SUCCESS lifecycle outcome. The
1026
+ // execute-turn-failure, normalization-failure, and tool-failure
1027
+ // branches emit TurnAbortedEvent instead (see earlier branches).
1028
+ status: AGENT_RUNTIME_TURN_COMPLETED_STATUSES.SUCCESS,
1021
1029
  cost_breakdown: createZeroTurnCostBreakdown(),
1022
1030
  }),
1023
1031
  );
@@ -1077,6 +1085,12 @@ async function runGovernedAgentLoopInternal(
1077
1085
  historyEntry.tool_call_id = toolCallId;
1078
1086
  historyEntry.tool_output = toolOutput;
1079
1087
 
1088
+ // WU-2829: approved=false when the tool-host blocked pending
1089
+ // approval (a paired APPROVAL_REQUIRED event follows). Every other
1090
+ // reachable branch means the call proceeded, so approved=true.
1091
+ const toolCallApproved = !(
1092
+ !toolOutput.success && toolOutput.error?.code === TOOL_ERROR_CODES.APPROVAL_REQUIRED
1093
+ );
1080
1094
  emitAgentRuntimeEvent(
1081
1095
  input.eventSink,
1082
1096
  buildToolCalledEvent({
@@ -1084,6 +1098,7 @@ async function runGovernedAgentLoopInternal(
1084
1098
  turn_index: currentTurnIndex,
1085
1099
  tool_name: normalizedTurn.requested_tool.name,
1086
1100
  tool_call_id: toolCallId,
1101
+ approved: toolCallApproved,
1087
1102
  }),
1088
1103
  );
1089
1104
 
@@ -1126,6 +1141,16 @@ async function runGovernedAgentLoopInternal(
1126
1141
  budget_name: 'max_turns_per_invocation',
1127
1142
  threshold: maxTurns,
1128
1143
  observed_value: invocationTurnCount,
1144
+ // WU-2829: we reach this emission only after the invocation turn
1145
+ // budget is saturated — level is EXCEEDED, remaining is 0, and
1146
+ // the scope is the session that ran out of turn budget. This
1147
+ // budget is in turns, not USD; remaining_usd mirrors "0 headroom
1148
+ // in the counted unit" and cloud renders the context via
1149
+ // budget_name.
1150
+ level: AGENT_RUNTIME_BUDGET_LEVELS.EXCEEDED,
1151
+ remaining_usd: 0,
1152
+ scope: AGENT_RUNTIME_BUDGET_SCOPES.SESSION,
1153
+ scope_id: sessionId,
1129
1154
  }),
1130
1155
  );
1131
1156
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumenflow/packs-agent-runtime",
3
- "version": "5.1.0",
3
+ "version": "5.2.1",
4
4
  "description": "Agent runtime pack scaffold for LumenFlow — governed model-turn execution, pack config, and provider capability baselines",
5
5
  "keywords": [
6
6
  "lumenflow",
@@ -70,6 +70,12 @@ function emitToolCalled(
70
70
  turn_index: REMOTE_CONTROL_TURN_INDEX,
71
71
  tool_name: toolName,
72
72
  tool_call_id: randomUUID(),
73
+ // WU-2829: remote-control invocations are governed by the
74
+ // conductor's approval flow BEFORE the kernel-side operation
75
+ // runs. By the time this emission fires, the operation has
76
+ // already been authorised — approved is always true at this
77
+ // call site.
78
+ approved: true,
73
79
  }),
74
80
  );
75
81
  }
@@ -111,6 +111,10 @@ export class MockRemoteControlAdapter implements RemoteControlPort {
111
111
  turn_index: MOCK_TURN_INDEX,
112
112
  tool_name: toolName,
113
113
  tool_call_id: randomUUID(),
114
+ // WU-2829: mock fixture mirrors the real-adapter contract —
115
+ // remote-control operations emit tool_called after the
116
+ // conductor's approval flow has authorised the invocation.
117
+ approved: true,
114
118
  }),
115
119
  );
116
120
  }
@@ -69,6 +69,62 @@ export interface AgentRuntimeTurnCostBreakdown {
69
69
 
70
70
  export type AgentRuntimeCleanupStatus = 'clean' | 'partial' | 'needs_recovery';
71
71
 
72
+ /**
73
+ * WU-2829 (INIT-062 WU-C): narrowed TurnCompletedEvent.status so cloud
74
+ * can resolve the turn discriminant without runtime string checks. The
75
+ * four values describe the lifecycle outcome of a turn; the internal
76
+ * per-turn semantic status (`reply | tool_request | complete | escalate`)
77
+ * remains on AgentRuntimeExecuteTurnOutput — it is NOT the same concept.
78
+ */
79
+ export const AGENT_RUNTIME_TURN_COMPLETED_STATUSES = {
80
+ SUCCESS: 'success',
81
+ ERROR: 'error',
82
+ ABORTED: 'aborted',
83
+ PAUSED: 'paused',
84
+ } as const;
85
+
86
+ export const AGENT_RUNTIME_TURN_COMPLETED_STATUS_VALUES = [
87
+ AGENT_RUNTIME_TURN_COMPLETED_STATUSES.SUCCESS,
88
+ AGENT_RUNTIME_TURN_COMPLETED_STATUSES.ERROR,
89
+ AGENT_RUNTIME_TURN_COMPLETED_STATUSES.ABORTED,
90
+ AGENT_RUNTIME_TURN_COMPLETED_STATUSES.PAUSED,
91
+ ] as const;
92
+
93
+ export type AgentRuntimeTurnCompletedStatus =
94
+ (typeof AGENT_RUNTIME_TURN_COMPLETED_STATUS_VALUES)[number];
95
+
96
+ /**
97
+ * WU-2829 (INIT-062 WU-C): budget threshold enrichment. Cloud previously
98
+ * had to reconstruct `level` from `threshold`/`observed_value`; we now
99
+ * supply the discriminant directly alongside `remaining_usd`, `scope`,
100
+ * and `scope_id` so dashboards render without post-hoc inference.
101
+ */
102
+ export const AGENT_RUNTIME_BUDGET_LEVELS = {
103
+ WARNING: 'warning',
104
+ EXCEEDED: 'exceeded',
105
+ } as const;
106
+
107
+ export const AGENT_RUNTIME_BUDGET_LEVEL_VALUES = [
108
+ AGENT_RUNTIME_BUDGET_LEVELS.WARNING,
109
+ AGENT_RUNTIME_BUDGET_LEVELS.EXCEEDED,
110
+ ] as const;
111
+
112
+ export type AgentRuntimeBudgetLevel = (typeof AGENT_RUNTIME_BUDGET_LEVEL_VALUES)[number];
113
+
114
+ export const AGENT_RUNTIME_BUDGET_SCOPES = {
115
+ SESSION: 'session',
116
+ WORKSPACE: 'workspace',
117
+ ORG: 'org',
118
+ } as const;
119
+
120
+ export const AGENT_RUNTIME_BUDGET_SCOPE_VALUES = [
121
+ AGENT_RUNTIME_BUDGET_SCOPES.SESSION,
122
+ AGENT_RUNTIME_BUDGET_SCOPES.WORKSPACE,
123
+ AGENT_RUNTIME_BUDGET_SCOPES.ORG,
124
+ ] as const;
125
+
126
+ export type AgentRuntimeBudgetScope = (typeof AGENT_RUNTIME_BUDGET_SCOPE_VALUES)[number];
127
+
72
128
  export interface TurnStartedEvent extends AgentRuntimeEventEnvelope {
73
129
  kind: typeof AGENT_RUNTIME_EVENT_KINDS.TURN_STARTED;
74
130
  session_id: string;
@@ -80,7 +136,9 @@ export interface TurnCompletedEvent extends AgentRuntimeEventEnvelope {
80
136
  kind: typeof AGENT_RUNTIME_EVENT_KINDS.TURN_COMPLETED;
81
137
  session_id: string;
82
138
  turn_index: number;
83
- status: string;
139
+ // WU-2829: narrowed from `string` to the four lifecycle outcomes so
140
+ // cloud gets TS exhaustiveness on the discriminated union.
141
+ status: AgentRuntimeTurnCompletedStatus;
84
142
  cost_breakdown: AgentRuntimeTurnCostBreakdown;
85
143
  }
86
144
 
@@ -99,6 +157,15 @@ export interface ToolCalledEvent extends AgentRuntimeEventEnvelope {
99
157
  turn_index: number;
100
158
  tool_name: string;
101
159
  tool_call_id: string;
160
+ /**
161
+ * WU-2829 (INIT-062 WU-C): approval discriminant so cloud no longer
162
+ * has to cross-reference APPROVAL_REQUIRED events to tell which tool
163
+ * calls were gated. `true` = call proceeded (either no approval
164
+ * required, or approval was satisfied); `false` = call blocked
165
+ * pending an approval (paired with a subsequent APPROVAL_REQUIRED
166
+ * event carrying the request_id).
167
+ */
168
+ approved: boolean;
102
169
  }
103
170
 
104
171
  export interface ApprovalRequiredEvent extends AgentRuntimeEventEnvelope {
@@ -149,6 +216,28 @@ export interface BudgetThresholdEvent extends AgentRuntimeEventEnvelope {
149
216
  budget_name: string;
150
217
  threshold: number;
151
218
  observed_value: number;
219
+ /**
220
+ * WU-2829 (INIT-062 WU-C): discriminant for cloud dashboards so the
221
+ * level is no longer inferred from threshold/observed_value.
222
+ */
223
+ level: AgentRuntimeBudgetLevel;
224
+ /**
225
+ * WU-2829 (INIT-062 WU-C): remaining USD headroom for the budget at
226
+ * emission time. Negative values are permitted when the budget is
227
+ * already exceeded.
228
+ */
229
+ remaining_usd: number;
230
+ /**
231
+ * WU-2829 (INIT-062 WU-C): budget scope so cloud can route the signal
232
+ * to the correct dashboard (session / workspace / org).
233
+ */
234
+ scope: AgentRuntimeBudgetScope;
235
+ /**
236
+ * WU-2829 (INIT-062 WU-C): identifier of the scope object (session id,
237
+ * workspace id, or org id) — paired with `scope` to pin the emission
238
+ * to a specific subject.
239
+ */
240
+ scope_id: string;
152
241
  }
153
242
 
154
243
  export interface AgentSessionEnrolledEvent extends AgentRuntimeEventEnvelope {
@@ -678,6 +678,9 @@ emitted_event_kinds:
678
678
  - sidekick:routine_executed
679
679
  - sidekick:routine_step_failed
680
680
  - sidekick:state_rehydrated
681
+ - sidekick:state_rehydration_started
682
+ - sidekick:state_rehydration_chunk
683
+ - sidekick:state_rehydration_completed
681
684
  backpressure_policy:
682
685
  sidekick:task_created: queue-with-replay
683
686
  sidekick:task_completed: queue-with-replay
@@ -695,6 +698,9 @@ backpressure_policy:
695
698
  sidekick:routine_executed: queue-with-replay
696
699
  sidekick:routine_step_failed: queue-with-replay
697
700
  sidekick:state_rehydrated: queue-with-replay
701
+ sidekick:state_rehydration_started: queue-with-replay
702
+ sidekick:state_rehydration_chunk: queue-with-replay
703
+ sidekick:state_rehydration_completed: queue-with-replay
698
704
  state_aliases: {}
699
705
  lane_templates: []
700
706
  # WU-2735 (INIT-060 WU-7a, ADR-013 §ChannelBridge): the sidekick pack requires
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumenflow/packs-sidekick",
3
- "version": "5.1.0",
3
+ "version": "5.2.1",
4
4
  "description": "Sidekick personal assistant pack for LumenFlow — 16 tools for task management, typed memory, channels, routines, and audit",
5
5
  "keywords": [
6
6
  "lumenflow",
@@ -53,6 +53,7 @@
53
53
  "typecheck": "tsc --noEmit"
54
54
  },
55
55
  "dependencies": {
56
+ "@lumenflow/conductor-sdk": "workspace:^",
56
57
  "@lumenflow/kernel": "workspace:^"
57
58
  },
58
59
  "devDependencies": {
@@ -12,6 +12,7 @@ import {
12
12
  type MemoryRecord,
13
13
  type RoutineRecord,
14
14
  type SidekickStores,
15
+ type StoreName,
15
16
  type TaskRecord,
16
17
  } from './tool-impl/storage.js';
17
18
 
@@ -45,7 +46,17 @@ export const SIDEKICK_EVENT_KINDS = {
45
46
  ROUTINE_COMMITTED: 'sidekick:routine_committed',
46
47
  ROUTINE_EXECUTED: 'sidekick:routine_executed',
47
48
  ROUTINE_STEP_FAILED: 'sidekick:routine_step_failed',
49
+ /**
50
+ * @deprecated WU-2830 (INIT-062 WU-D): `sidekick:state_rehydrated` is
51
+ * unbounded and cannot stream. New emitters MUST use the chunked trio
52
+ * (`STATE_REHYDRATION_STARTED`, `STATE_REHYDRATION_CHUNK`,
53
+ * `STATE_REHYDRATION_COMPLETED`). The constant and type are retained for
54
+ * backwards compatibility only.
55
+ */
48
56
  STATE_REHYDRATED: 'sidekick:state_rehydrated',
57
+ STATE_REHYDRATION_STARTED: 'sidekick:state_rehydration_started',
58
+ STATE_REHYDRATION_CHUNK: 'sidekick:state_rehydration_chunk',
59
+ STATE_REHYDRATION_COMPLETED: 'sidekick:state_rehydration_completed',
49
60
  } as const;
50
61
 
51
62
  export const SIDEKICK_EVENT_KIND_VALUES = [
@@ -65,6 +76,9 @@ export const SIDEKICK_EVENT_KIND_VALUES = [
65
76
  SIDEKICK_EVENT_KINDS.ROUTINE_EXECUTED,
66
77
  SIDEKICK_EVENT_KINDS.ROUTINE_STEP_FAILED,
67
78
  SIDEKICK_EVENT_KINDS.STATE_REHYDRATED,
79
+ SIDEKICK_EVENT_KINDS.STATE_REHYDRATION_STARTED,
80
+ SIDEKICK_EVENT_KINDS.STATE_REHYDRATION_CHUNK,
81
+ SIDEKICK_EVENT_KINDS.STATE_REHYDRATION_COMPLETED,
68
82
  ] as const;
69
83
 
70
84
  export type SidekickEventKind = (typeof SIDEKICK_EVENT_KIND_VALUES)[number];
@@ -93,6 +107,9 @@ export const SIDEKICK_EVENT_BACKPRESSURE_POLICY: Record<
93
107
  [SIDEKICK_EVENT_KINDS.ROUTINE_EXECUTED]: QUEUE_WITH_REPLAY,
94
108
  [SIDEKICK_EVENT_KINDS.ROUTINE_STEP_FAILED]: QUEUE_WITH_REPLAY,
95
109
  [SIDEKICK_EVENT_KINDS.STATE_REHYDRATED]: QUEUE_WITH_REPLAY,
110
+ [SIDEKICK_EVENT_KINDS.STATE_REHYDRATION_STARTED]: QUEUE_WITH_REPLAY,
111
+ [SIDEKICK_EVENT_KINDS.STATE_REHYDRATION_CHUNK]: QUEUE_WITH_REPLAY,
112
+ [SIDEKICK_EVENT_KINDS.STATE_REHYDRATION_COMPLETED]: QUEUE_WITH_REPLAY,
96
113
  };
97
114
 
98
115
  interface SidekickEventEnvelope {
@@ -144,13 +161,23 @@ export interface MemoryForgottenEvent extends SidekickEventEnvelope {
144
161
 
145
162
  export interface ChannelMessageSentEvent extends SidekickEventEnvelope {
146
163
  kind: typeof SIDEKICK_EVENT_KINDS.CHANNEL_MESSAGE_SENT;
147
- message: Record<string, unknown>;
164
+ /**
165
+ * WU-2830 (INIT-062 WU-D): typed as {@link ChannelMessageRecord} for
166
+ * symmetry with {@link ChannelMessageReceivedEvent.messages} and to enable
167
+ * static analysis cloud-side.
168
+ */
169
+ message: ChannelMessageRecord;
148
170
  }
149
171
 
150
172
  export interface ChannelMessageReceivedEvent extends SidekickEventEnvelope {
151
173
  kind: typeof SIDEKICK_EVENT_KINDS.CHANNEL_MESSAGE_RECEIVED;
152
174
  channel: string;
153
- count: number;
175
+ /**
176
+ * WU-2830 (INIT-062 WU-D): full {@link ChannelMessageRecord} payloads
177
+ * replace the previous bare `count` field. Subscribers count via
178
+ * `messages.length`; symmetric with {@link ChannelMessageSentEvent.message}.
179
+ */
180
+ messages: ChannelMessageRecord[];
154
181
  provider?: string;
155
182
  }
156
183
 
@@ -199,11 +226,50 @@ export interface RoutineStepFailedEvent extends SidekickEventEnvelope {
199
226
  routine_name?: string;
200
227
  }
201
228
 
229
+ /**
230
+ * @deprecated WU-2830 (INIT-062 WU-D): unbounded snapshots cannot stream.
231
+ * Emit {@link StateRehydrationStartedEvent} +
232
+ * {@link StateRehydrationChunkEvent} (one per store) +
233
+ * {@link StateRehydrationCompletedEvent} instead. Retained for parity with
234
+ * existing consumers and the compile-time parity tests.
235
+ */
202
236
  export interface StateRehydratedEvent extends SidekickEventEnvelope {
203
237
  kind: typeof SIDEKICK_EVENT_KINDS.STATE_REHYDRATED;
204
238
  snapshot: SidekickStores;
205
239
  }
206
240
 
241
+ export interface StateRehydrationStartedEvent extends SidekickEventEnvelope {
242
+ kind: typeof SIDEKICK_EVENT_KINDS.STATE_REHYDRATION_STARTED;
243
+ /** Correlation id shared by the started/chunk/completed trio. */
244
+ rehydration_id: string;
245
+ /** Total chunks the receiver should expect. */
246
+ total_chunks: number;
247
+ }
248
+
249
+ export interface StateRehydrationChunkEvent extends SidekickEventEnvelope {
250
+ kind: typeof SIDEKICK_EVENT_KINDS.STATE_REHYDRATION_CHUNK;
251
+ /** Correlation id matching the {@link StateRehydrationStartedEvent}. */
252
+ rehydration_id: string;
253
+ /**
254
+ * Monotonic zero-based index identifying this chunk's position within the
255
+ * stream. Receivers reassemble ordered by `cursor`.
256
+ */
257
+ cursor: number;
258
+ /** True for the final chunk in the stream (cursor === total_chunks - 1). */
259
+ last_chunk: boolean;
260
+ /**
261
+ * Subset of {@link SidekickStores} keys carried by this chunk. Receivers
262
+ * merge `stores` into the accumulating snapshot keyed on store name.
263
+ */
264
+ stores: Partial<SidekickStores>;
265
+ }
266
+
267
+ export interface StateRehydrationCompletedEvent extends SidekickEventEnvelope {
268
+ kind: typeof SIDEKICK_EVENT_KINDS.STATE_REHYDRATION_COMPLETED;
269
+ rehydration_id: string;
270
+ chunk_count: number;
271
+ }
272
+
207
273
  export type SidekickEvent =
208
274
  | TaskCreatedEvent
209
275
  | TaskCompletedEvent
@@ -220,7 +286,10 @@ export type SidekickEvent =
220
286
  | RoutineCommittedEvent
221
287
  | RoutineExecutedEvent
222
288
  | RoutineStepFailedEvent
223
- | StateRehydratedEvent;
289
+ | StateRehydratedEvent
290
+ | StateRehydrationStartedEvent
291
+ | StateRehydrationChunkEvent
292
+ | StateRehydrationCompletedEvent;
224
293
 
225
294
  const channelSeqCounters = new Map<string, number>();
226
295
 
@@ -406,7 +475,7 @@ export function buildMemoryForgottenEvent(memory_id: string): MemoryForgottenEve
406
475
  }
407
476
 
408
477
  export function buildChannelMessageSentEvent(
409
- message: Record<string, unknown>,
478
+ message: ChannelMessageRecord,
410
479
  ): ChannelMessageSentEvent {
411
480
  return stampSidekickEvent<ChannelMessageSentEvent>({
412
481
  kind: SIDEKICK_EVENT_KINDS.CHANNEL_MESSAGE_SENT,
@@ -416,13 +485,13 @@ export function buildChannelMessageSentEvent(
416
485
 
417
486
  export function buildChannelMessageReceivedEvent(input: {
418
487
  channel: string;
419
- count: number;
488
+ messages: ChannelMessageRecord[];
420
489
  provider?: string;
421
490
  }): ChannelMessageReceivedEvent {
422
491
  return stampSidekickEvent<ChannelMessageReceivedEvent>({
423
492
  kind: SIDEKICK_EVENT_KINDS.CHANNEL_MESSAGE_RECEIVED,
424
493
  channel: input.channel,
425
- count: input.count,
494
+ messages: input.messages,
426
495
  ...(input.provider ? { provider: input.provider } : {}),
427
496
  });
428
497
  }
@@ -495,6 +564,11 @@ export function buildRoutineStepFailedEvent(input: {
495
564
  });
496
565
  }
497
566
 
567
+ /**
568
+ * @deprecated WU-2830 (INIT-062 WU-D): prefer
569
+ * {@link buildStateRehydrationChunkEvents} which emits the bounded
570
+ * started/chunk/completed trio. Retained for backwards compatibility.
571
+ */
498
572
  export function buildStateRehydratedEvent(snapshot: SidekickStores): StateRehydratedEvent {
499
573
  return stampSidekickEvent<StateRehydratedEvent>({
500
574
  kind: SIDEKICK_EVENT_KINDS.STATE_REHYDRATED,
@@ -502,16 +576,119 @@ export function buildStateRehydratedEvent(snapshot: SidekickStores): StateRehydr
502
576
  });
503
577
  }
504
578
 
505
- export function serializeLocalChannelMessage(
506
- message: ChannelMessageRecord,
507
- channelName?: string,
508
- ): Record<string, unknown> {
509
- return {
510
- id: message.id,
511
- channel_id: message.channel_id,
512
- ...(channelName ? { channel: channelName } : {}),
513
- sender: message.sender,
514
- content: message.content,
515
- created_at: message.created_at,
516
- };
579
+ export function buildStateRehydrationStartedEvent(input: {
580
+ rehydration_id: string;
581
+ total_chunks: number;
582
+ }): StateRehydrationStartedEvent {
583
+ return stampSidekickEvent<StateRehydrationStartedEvent>({
584
+ kind: SIDEKICK_EVENT_KINDS.STATE_REHYDRATION_STARTED,
585
+ rehydration_id: input.rehydration_id,
586
+ total_chunks: input.total_chunks,
587
+ });
588
+ }
589
+
590
+ export function buildStateRehydrationChunkEvent(input: {
591
+ rehydration_id: string;
592
+ cursor: number;
593
+ last_chunk: boolean;
594
+ stores: Partial<SidekickStores>;
595
+ }): StateRehydrationChunkEvent {
596
+ return stampSidekickEvent<StateRehydrationChunkEvent>({
597
+ kind: SIDEKICK_EVENT_KINDS.STATE_REHYDRATION_CHUNK,
598
+ rehydration_id: input.rehydration_id,
599
+ cursor: input.cursor,
600
+ last_chunk: input.last_chunk,
601
+ stores: input.stores,
602
+ });
603
+ }
604
+
605
+ export function buildStateRehydrationCompletedEvent(input: {
606
+ rehydration_id: string;
607
+ chunk_count: number;
608
+ }): StateRehydrationCompletedEvent {
609
+ return stampSidekickEvent<StateRehydrationCompletedEvent>({
610
+ kind: SIDEKICK_EVENT_KINDS.STATE_REHYDRATION_COMPLETED,
611
+ rehydration_id: input.rehydration_id,
612
+ chunk_count: input.chunk_count,
613
+ });
614
+ }
615
+
616
+ /**
617
+ * WU-2830 (INIT-062 WU-D): the store order used by the chunked rehydration
618
+ * emitter. Exposed so receivers that care about deterministic reassembly
619
+ * can cross-check cursor semantics.
620
+ */
621
+ export const SIDEKICK_REHYDRATION_STORE_ORDER = [
622
+ 'tasks',
623
+ 'memories',
624
+ 'channels',
625
+ 'messages',
626
+ 'routines',
627
+ ] as const satisfies readonly StoreName[];
628
+
629
+ export type SidekickRehydrationStore = (typeof SIDEKICK_REHYDRATION_STORE_ORDER)[number];
630
+
631
+ /**
632
+ * WU-2830 (INIT-062 WU-D): emit the chunked rehydration stream.
633
+ *
634
+ * Emission order: `state_rehydration_started` → one
635
+ * `state_rehydration_chunk` per store in
636
+ * {@link SIDEKICK_REHYDRATION_STORE_ORDER} → `state_rehydration_completed`.
637
+ * `cursor` is a zero-based monotonic index; `last_chunk` is true on the final
638
+ * chunk so streaming receivers can close the reassembly window without
639
+ * waiting for the completion event.
640
+ */
641
+ export async function emitSidekickStateRehydration(snapshot: SidekickStores): Promise<{
642
+ rehydration_id: string;
643
+ chunk_count: number;
644
+ }> {
645
+ const rehydration_id = randomUUID();
646
+ const stores = SIDEKICK_REHYDRATION_STORE_ORDER;
647
+ const total_chunks = stores.length;
648
+
649
+ await emitSidekickEvent(buildStateRehydrationStartedEvent({ rehydration_id, total_chunks }));
650
+
651
+ for (let cursor = 0; cursor < stores.length; cursor++) {
652
+ const store = stores[cursor];
653
+ if (store === undefined) {
654
+ continue;
655
+ }
656
+ const last_chunk = cursor === stores.length - 1;
657
+ const chunkStores: Partial<SidekickStores> = {};
658
+ // Narrow the union so TS keeps the store-value type aligned with the key.
659
+ switch (store) {
660
+ case 'tasks':
661
+ chunkStores.tasks = snapshot.tasks;
662
+ break;
663
+ case 'memories':
664
+ chunkStores.memories = snapshot.memories;
665
+ break;
666
+ case 'channels':
667
+ chunkStores.channels = snapshot.channels;
668
+ break;
669
+ case 'messages':
670
+ chunkStores.messages = snapshot.messages;
671
+ break;
672
+ case 'routines':
673
+ chunkStores.routines = snapshot.routines;
674
+ break;
675
+ }
676
+ await emitSidekickEvent(
677
+ buildStateRehydrationChunkEvent({
678
+ rehydration_id,
679
+ cursor,
680
+ last_chunk,
681
+ stores: chunkStores,
682
+ }),
683
+ );
684
+ }
685
+
686
+ await emitSidekickEvent(
687
+ buildStateRehydrationCompletedEvent({
688
+ rehydration_id,
689
+ chunk_count: total_chunks,
690
+ }),
691
+ );
692
+
693
+ return { rehydration_id, chunk_count: total_chunks };
517
694
  }
@@ -294,18 +294,26 @@ export function createControlPlaneChannelBridge(
294
294
  // Port methods
295
295
  // -------------------------------------------------------------------------
296
296
 
297
+ async function registerInternal(bridgeConfig: BridgeConfig): Promise<ChannelId> {
298
+ const key = `${bridgeConfig.provider}::${bridgeConfig.name}::${hashOptions(
299
+ bridgeConfig.options as Record<string, unknown> | undefined,
300
+ )}`;
301
+ const existing = registry.get(key);
302
+ if (existing) {
303
+ return existing;
304
+ }
305
+ const id = mintChannelId(bridgeConfig);
306
+ registry.set(key, id);
307
+ return id;
308
+ }
309
+
297
310
  return {
311
+ async connect(bridgeConfig: BridgeConfig): Promise<ChannelId> {
312
+ return registerInternal(bridgeConfig);
313
+ },
314
+
298
315
  async register(bridgeConfig: BridgeConfig): Promise<ChannelId> {
299
- const key = `${bridgeConfig.provider}::${bridgeConfig.name}::${hashOptions(
300
- bridgeConfig.options as Record<string, unknown> | undefined,
301
- )}`;
302
- const existing = registry.get(key);
303
- if (existing) {
304
- return existing;
305
- }
306
- const id = mintChannelId(bridgeConfig);
307
- registry.set(key, id);
308
- return id;
316
+ return registerInternal(bridgeConfig);
309
317
  },
310
318
 
311
319
  async send(channelId: ChannelId, envelope: ChannelEnvelope): Promise<SendResult> {
@@ -180,6 +180,10 @@ export function createFilesystemChannelBridge(
180
180
  }
181
181
 
182
182
  return {
183
+ async connect(bridgeConfig: BridgeConfig): Promise<ChannelId> {
184
+ return ensureRegistered(bridgeConfig);
185
+ },
186
+
183
187
  async register(bridgeConfig: BridgeConfig): Promise<ChannelId> {
184
188
  return ensureRegistered(bridgeConfig);
185
189
  },