@deepseek-ai/dsh-api-session-controller 0.1.6-alpha.2 → 0.1.7-alpha.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 (36) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +31 -8
  3. package/README.zh.md +31 -8
  4. package/lib/client.js +603 -515
  5. package/lib/index.js +299 -137
  6. package/lib/typert.host.js +373 -183
  7. package/lib/typert.remote-client.d.ts +10 -5
  8. package/lib/typert.remote-client.js +216 -124
  9. package/lib/types/archived-session-gate.d.ts +30 -0
  10. package/lib/types/archived-session-gate.js +52 -0
  11. package/lib/types/client/contract/session.d.ts +2 -0
  12. package/lib/types/client/contract/sessions.d.ts +11 -16
  13. package/lib/types/client/index.d.ts +1 -1
  14. package/lib/types/client/sessions/lineage.d.ts +4 -1
  15. package/lib/types/client/sessions/lineage.js +2 -1
  16. package/lib/types/client/sessions/manager.d.ts +66 -59
  17. package/lib/types/client/sessions/manager.js +249 -277
  18. package/lib/types/client/sessions/projection-store.d.ts +32 -13
  19. package/lib/types/client/sessions/projection-store.js +45 -13
  20. package/lib/types/client/sessions/remotes.d.ts +1 -2
  21. package/lib/types/client/sessions/service.d.ts +23 -35
  22. package/lib/types/client/sessions/service.js +35 -47
  23. package/lib/types/client/sessions/session.d.ts +18 -16
  24. package/lib/types/client/sessions/session.js +112 -50
  25. package/lib/types/commands.d.ts +4 -2
  26. package/lib/types/commands.js +72 -36
  27. package/lib/types/control.d.ts +2 -4
  28. package/lib/types/control.js +2 -44
  29. package/lib/types/history.js +1 -1
  30. package/lib/types/index.d.ts +32 -5
  31. package/lib/types/index.js +117 -13
  32. package/lib/types/list.js +24 -15
  33. package/lib/types/skill-catalog.js +60 -50
  34. package/lib/types/types.d.ts +37 -23
  35. package/lib/types/types.js +0 -1
  36. package/package.json +71 -76
@@ -43,6 +43,7 @@ export class Session {
43
43
  jumpTargetSeq = null;
44
44
  /** The running jump loop's completion, shared by retargeting callers. */
45
45
  jumpPromise = null;
46
+ pendingHistory = null;
46
47
  stopObservingInbox;
47
48
  assistantStream = new ClientAssistantStream();
48
49
  running = false;
@@ -56,7 +57,7 @@ export class Session {
56
57
  promptAttempted = false;
57
58
  /** A first accepted prompt stays in the engaging phase until its turn is observable. */
58
59
  firstPromptPendingTurn = false;
59
- /** Empty-log mirror (see ConversationSnapshot.blank); unknown bare sessions begin conservatively blank. */
60
+ /** New Session display state; unknown bare sessions begin conservatively blank. */
60
61
  blankBit = true;
61
62
  removed = false;
62
63
  promptError = null;
@@ -72,8 +73,9 @@ export class Session {
72
73
  * Per-session projection value store (push model; see the session-projection
73
74
  * subsystem page, docs/subsystems/session-projection.md): finished whole
74
75
  * values computed on the Host, seeded by the tail page's
75
- * projections block and updated by Session Controller control frames under the
76
- * one higher-seq-wins rule. Keys are read via `projections.faceOf(key)`
76
+ * projections block and updated by Session Controller control frames;
77
+ * Host-sequenced writes merge under higher-seq-wins and cached list blocks
78
+ * yield to them (projection-store.ts). Keys are read via `projections.faceOf(key)`
77
79
  * (the useProjection resolution face); the conversation snapshot never
78
80
  * carries projection values, and no client-side domain folding exists.
79
81
  * Manager-owned when constructed through SessionManager (frames route and
@@ -140,16 +142,15 @@ export class Session {
140
142
  */
141
143
  beginSubmission(input) {
142
144
  const requestId = randomUUID();
145
+ const placement = this.running ? input.mode === 'steer' ? 'steering' : 'queued' : 'transcript';
143
146
  this.pendingSubmissions = [...this.pendingSubmissions, {
144
147
  requestId,
145
- placement: this.running
146
- ? input.mode === 'steer' ? 'steering' : 'queued'
147
- : 'transcript',
148
+ placement,
148
149
  time: Date.now(),
149
150
  text: input.text,
150
151
  attachments: input.attachments,
151
152
  }];
152
- this.submissionSettlements.set(requestId, { onRetire: input.onRetire, retiring: false });
153
+ this.submissionSettlements.set(requestId, { placement, onRetire: input.onRetire, retiring: false });
153
154
  // The blank → engaging edge flips here, ahead of prompt(): the composer
154
155
  // docks and the echo renders on the click's own frame.
155
156
  this.promptAttempted = true;
@@ -213,19 +214,12 @@ export class Session {
213
214
  this.notifier.markDirty();
214
215
  return result;
215
216
  }
216
- // Blank flips on ACCEPTANCE, not attempt: an accepted prompt starts the
217
- // conversation's first turn on the host (the host criterion — a logged
218
- // turn/start — is fact, not optimism; standalone command and projection
219
- // events never flip it), while a rejected first prompt must keep the
220
- // session blank — the client-side blank mirror only ever lowers, so
221
- // flipping early on a failure would surface the session forever and
222
- // strip its connectWorkspace reuse eligibility against the host's
223
- // authority.
217
+ // Rejection must leave a first prompt blank and eligible for workspace reuse.
224
218
  if (this.blankBit) {
225
219
  this.blankBit = false;
226
- this.options.onEngaged?.(this);
227
220
  this.notifier.markDirty();
228
221
  }
222
+ this.options.onEngaged?.(this);
229
223
  return result;
230
224
  }
231
225
  /**
@@ -347,6 +341,15 @@ export class Session {
347
341
  // loop starts here.
348
342
  if (this.loadingOlder)
349
343
  return Promise.resolve();
344
+ const events = this.events;
345
+ if (events === undefined)
346
+ return Promise.resolve();
347
+ const pending = {
348
+ beforeSeq: this.baseSeq,
349
+ hasMore: this.hasMore,
350
+ pages: [],
351
+ };
352
+ this.pendingHistory = pending;
350
353
  this.jumpTargetSeq = seq;
351
354
  this.loadingOlder = true;
352
355
  this.notifier.markDirty();
@@ -356,17 +359,14 @@ export class Session {
356
359
  const generation = this.openGeneration;
357
360
  this.jumpPromise = (async () => {
358
361
  try {
359
- while (this.hasMore && this.jumpTargetSeq !== null && this.baseSeq > this.jumpTargetSeq) {
362
+ while (pending.hasMore && this.jumpTargetSeq !== null && pending.beforeSeq > this.jumpTargetSeq) {
360
363
  if (generation !== this.openGeneration)
361
364
  return;
362
- const events = this.events;
363
- if (events === undefined)
364
- return;
365
- const before = this.baseSeq;
366
- await events.prepend({ beforeSeq: this.baseSeq, maxMessages: JUMP_PAGE_MESSAGES });
365
+ const before = pending.beforeSeq;
366
+ await events.prepend({ beforeSeq: before, maxMessages: JUMP_PAGE_MESSAGES });
367
367
  // No-progress guard: an empty or dropped page that still claims more
368
368
  // history must end the loop, not spin it.
369
- if (this.baseSeq >= before)
369
+ if (pending.beforeSeq >= before)
370
370
  return;
371
371
  }
372
372
  }
@@ -378,7 +378,11 @@ export class Session {
378
378
  finally {
379
379
  this.jumpTargetSeq = null;
380
380
  this.jumpPromise = null;
381
+ this.pendingHistory = null;
381
382
  this.loadingOlder = false;
383
+ if (generation === this.openGeneration && pending.pages.length > 0) {
384
+ this.prependWindow(pending.pages.reverse().flat(), pending.hasMore);
385
+ }
382
386
  this.notifier.markDirty();
383
387
  }
384
388
  })();
@@ -424,8 +428,7 @@ export class Session {
424
428
  * @param running - the new running state.
425
429
  */
426
430
  handleRunning(running) {
427
- // Turn-start conversion: a blank session never runs, so the first
428
- // running:true proves another side's first message landed.
431
+ // Running converts display state without establishing durable turn history.
429
432
  if (running && this.blankBit) {
430
433
  this.blankBit = false;
431
434
  this.notifier.markDirty();
@@ -465,13 +468,15 @@ export class Session {
465
468
  this.notifier.markDirty();
466
469
  }
467
470
  /**
468
- * Blank-bit relay from the authoritative summary source (`session.list` and
469
- * `api-session/added`). Monotone: once any signal (local first send,
470
- * running flip, an earlier summary) cleared it, a stale true never
471
- * re-blanks.
472
- * @param blank - the summary's derived empty-log bit.
471
+ * Apply the Manager's effective display blank, further reconciled with the
472
+ * current `sessionListMetadata` projection. Local send attempts and current
473
+ * running state prevent re-blanking; an earlier false summary alone does not.
474
+ * The Manager retains acceptance and earlier running observations across
475
+ * Session-object replacement.
476
+ * @param blank - New Session display state after Manager reconciliation.
473
477
  */
474
478
  handleBlank(blank) {
479
+ blank = blank && this.projections.values().sessionListMetadata?.blank !== false;
475
480
  if (blank === this.blankBit)
476
481
  return;
477
482
  if (blank && (this.promptAttempted || this.running))
@@ -570,13 +575,31 @@ export class Session {
570
575
  const visible = this.assistantStream.replace(entries, assistantStream);
571
576
  this.baseSeq = SessionLogOffset(entries[0]?.event.seq ?? 0);
572
577
  this.hasMore = hasMore;
578
+ if (this.pendingHistory !== null) {
579
+ this.pendingHistory.beforeSeq = this.baseSeq;
580
+ this.pendingHistory.hasMore = hasMore;
581
+ this.pendingHistory.pages.length = 0;
582
+ }
573
583
  if (visible.some(entry => entry.event.type === 'turn/start'))
574
584
  this.firstPromptPendingTurn = false;
575
585
  if (projections !== undefined)
576
586
  this.projections.seed(projections);
577
587
  this.eventSource.replace(visible, hasMore);
588
+ // A new follow baseline replaces optimistic steering with Host-owned rows.
589
+ // Receipt-backed inputs are accepted, not failed, even if their history is outside this window.
590
+ if (projections !== undefined) {
591
+ for (const [requestId, { receipt }] of this.submissionSettlements) {
592
+ if (receipt !== undefined && receipt.seq <= projections.asOfSeq) {
593
+ this.scheduleObservedRetirement(requestId, receipt.attachments);
594
+ }
595
+ }
596
+ }
578
597
  for (const entry of visible)
579
598
  this.observeSubmissionEvent(entry.event);
599
+ if (projections !== undefined) {
600
+ const inbox = projections.values.inbox;
601
+ this.observeSteeringInsertions(inbox?.['next-step'] ?? [], 0, projections.asOfSeq);
602
+ }
580
603
  this.notifier.markDirty();
581
604
  }
582
605
  publishAssistantEntry(result) {
@@ -609,6 +632,13 @@ export class Session {
609
632
  }
610
633
  /** Prepend one stream-validated history page. */
611
634
  prependWindow(entries, hasMore) {
635
+ if (this.pendingHistory !== null) {
636
+ const pending = this.pendingHistory;
637
+ pending.beforeSeq = entries[0] === undefined ? pending.beforeSeq : SessionLogOffset(entries[0].event.seq);
638
+ pending.hasMore = hasMore;
639
+ pending.pages.push(entries);
640
+ return;
641
+ }
612
642
  this.baseSeq = entries[0] === undefined ? this.baseSeq : SessionLogOffset(entries[0].event.seq);
613
643
  this.hasMore = hasMore;
614
644
  this.eventSource.prepend(entries, hasMore);
@@ -631,37 +661,66 @@ export class Session {
631
661
  if (this.submissionSettlements.size === 0)
632
662
  return;
633
663
  if (event.type === 'agent/inbox/spliced') {
634
- const splice = event.data;
635
- if (Array.isArray(splice?.inserted)) {
636
- for (const message of splice.inserted)
637
- this.observeSubmissionEvent({ type: 'user/message', data: message });
664
+ const { target, start, removedCount = 0, inserted, outcome } = event.data;
665
+ if (target === 'next-step') {
666
+ for (const [requestId, settlement] of this.submissionSettlements) {
667
+ const receipt = settlement.receipt;
668
+ if (receipt === undefined || receipt.index === null || receipt.seq >= event.seq)
669
+ continue;
670
+ const removed = receipt.index >= start && receipt.index < start + removedCount;
671
+ if (removed && outcome === 'canceled')
672
+ this.retireFailedSubmission(requestId);
673
+ else
674
+ settlement.receipt = {
675
+ ...receipt,
676
+ seq: event.seq,
677
+ index: removed ? null : receipt.index < start ? receipt.index : receipt.index + inserted.length - removedCount,
678
+ };
679
+ }
680
+ this.observeSteeringInsertions(inserted, start, event.seq);
681
+ }
682
+ for (const message of inserted)
683
+ this.observeSubmissionMessage(message, false);
684
+ return;
685
+ }
686
+ if (event.type === 'request/context' || event.type === 'turn/end') {
687
+ for (const [requestId, settlement] of this.submissionSettlements) {
688
+ if (settlement.receipt?.index === null && settlement.receipt.seq < event.seq)
689
+ this.retireFailedSubmission(requestId);
638
690
  }
639
691
  return;
640
692
  }
641
- if (event.type !== 'user/message')
693
+ if (event.type === 'user/message')
694
+ this.observeSubmissionMessage(event.data, true);
695
+ }
696
+ observeSteeringInsertions(messages, start, seq) {
697
+ for (const [index, message] of messages.entries()) {
698
+ const source = message.source;
699
+ if (source.kind !== 'user' || !('rpcId' in source))
700
+ continue;
701
+ const settlement = this.submissionSettlements.get(source.rpcId);
702
+ if (settlement?.placement !== 'steering' || settlement.retiring || (settlement.receipt?.seq ?? -1) > seq)
703
+ continue;
704
+ settlement.receipt = { seq, index: start + index, attachments: attachmentRefsIn(message.content) };
705
+ }
706
+ }
707
+ observeSubmissionMessage(message, admitted) {
708
+ const source = message.source;
709
+ if (source.kind !== 'user' || !('rpcId' in source))
642
710
  return;
643
- // Structural read: window entries may be compact history records, so the
644
- // fields are narrowed rather than trusted (same posture as Conversation
645
- // assembly matchers).
646
- const data = event.data;
647
- const source = data?.source;
648
- if (source?.kind !== 'user' || typeof source.rpcId !== 'string')
711
+ if (!admitted && this.submissionSettlements.get(source.rpcId)?.placement === 'steering')
649
712
  return;
650
- this.scheduleObservedRetirement(source.rpcId, attachmentRefsIn(data?.content));
713
+ this.scheduleObservedRetirement(source.rpcId, attachmentRefsIn(message.content));
651
714
  }
652
- /** Retire local echoes when their accepted messages appear in the durable Inbox projection. */
715
+ /** Retire non-steering echoes when the Inbox accepts their queue occurrences. */
653
716
  observeSubmissionInbox() {
654
717
  if (this.submissionSettlements.size === 0)
655
718
  return;
656
719
  const inbox = this.projections.get('inbox');
657
720
  if (inbox === undefined)
658
721
  return;
659
- for (const message of [...inbox['next-turn'], ...inbox['next-step']]) {
660
- const source = message.source;
661
- if (source.kind === 'user' && 'rpcId' in source) {
662
- this.scheduleObservedRetirement(source.rpcId, attachmentRefsIn(message.content));
663
- }
664
- }
722
+ for (const message of [...inbox['next-turn'], ...inbox['next-step']])
723
+ this.observeSubmissionMessage(message, false);
665
724
  }
666
725
  /**
667
726
  * Latch one observed settlement and remove the echo an animation frame
@@ -710,6 +769,7 @@ export class Session {
710
769
  this.notifier.markDirty();
711
770
  }
712
771
  buildSnapshot() {
772
+ const identity = this.projections.values().subagent;
713
773
  return {
714
774
  sessionId: this.sessionId,
715
775
  pendingSubmissions: this.pendingSubmissions,
@@ -717,7 +777,9 @@ export class Session {
717
777
  subagent: this.address === undefined
718
778
  ? null
719
779
  : {
720
- address: this.address,
780
+ address: this.address.mode === 'unknown' && identity != null
781
+ ? { ...this.address, mode: identity.mode }
782
+ : this.address,
721
783
  ...(this.parentAvailable === undefined ? {} : { parentAvailable: this.parentAvailable }),
722
784
  },
723
785
  removed: this.removed,
@@ -32,8 +32,10 @@ export declare class SessionCommandController {
32
32
  */
33
33
  rename(request: SessionRenameRequest): Promise<SessionRenameValue>;
34
34
  /**
35
- * Create a new ordinary Session from one completed-turn prefix.
36
- * @param request - source Session and optional event anchor.
35
+ * Create a new ordinary Session from an exact event prefix. An explicit
36
+ * `atSeq` is the inclusive cut; an omitted value selects the latest
37
+ * completed-turn prefix. An open cut receives synthetic fork closers.
38
+ * @param request - source Session and optional exact event boundary.
37
39
  * @returns the new Session identity.
38
40
  */
39
41
  fork(request: SessionForkRequest): Promise<SessionForkValue>;
@@ -55,6 +55,7 @@ import { randomUUID } from 'node:crypto';
55
55
  import { brandString } from '@deepseek-ai/dsh-brand';
56
56
  import { AttachmentError } from '@deepseek-ai/dsh-attachment';
57
57
  import { ReasoningEffortId, assistantStreamChunks, createUserMessage, freezeMessage, } from '@deepseek-ai/dsh-llm';
58
+ import { buildForkSeed } from '@deepseek-ai/dsh-session/fork';
58
59
  import { SessionLogOffset, SessionSeq } from '@deepseek-ai/dsh-session';
59
60
  import { SessionQueryError } from '@deepseek-ai/dsh-session-query';
60
61
  import { SessionTitleInvalidError } from '@deepseek-ai/dsh-session-title';
@@ -65,6 +66,23 @@ import { ApiSessionCwdConflict, ApiSessionNotFound, ApiSessionPresetConflict, Ap
65
66
  function hasPromptContent(content) {
66
67
  return content.some(part => part.type !== 'text' || part.text.trim().length > 0);
67
68
  }
69
+ /**
70
+ * Resolve the omitted-`atSeq` default to the latest completed-turn prefix,
71
+ * including standalone events before the next turn begins.
72
+ */
73
+ function latestCompletedPrefixBoundary(events) {
74
+ const lastTurnEnd = events.findLast(event => event.type === 'turn/end');
75
+ if (lastTurnEnd === undefined)
76
+ return undefined;
77
+ let boundary = lastTurnEnd.seq;
78
+ for (const next of events.slice(boundary + 1)) {
79
+ if (next.type === 'turn/start' || (next.type === 'user/message' && next.surfaceOp === 'append')
80
+ || next.type === 'agent/inbox/spliced')
81
+ break;
82
+ boundary = next.seq;
83
+ }
84
+ return boundary;
85
+ }
68
86
  /** Implements Session business commands delegated by the Session Controller Remote service. */
69
87
  export class SessionCommandController {
70
88
  ctx;
@@ -180,8 +198,10 @@ export class SessionCommandController {
180
198
  }
181
199
  }
182
200
  /**
183
- * Create a new ordinary Session from one completed-turn prefix.
184
- * @param request - source Session and optional event anchor.
201
+ * Create a new ordinary Session from an exact event prefix. An explicit
202
+ * `atSeq` is the inclusive cut; an omitted value selects the latest
203
+ * completed-turn prefix. An open cut receives synthetic fork closers.
204
+ * @param request - source Session and optional exact event boundary.
185
205
  * @returns the new Session identity.
186
206
  */
187
207
  async fork(request) {
@@ -208,20 +228,13 @@ export class SessionCommandController {
208
228
  throw new RemoteError('gateway/internal', `fork source unavailable for session "${request.sessionId}": ${String(error)}`, {});
209
229
  }
210
230
  const source = __addDisposableResource(env_1, observed, false);
211
- const lastSeq = source.events.at(-1)?.seq ?? -1;
212
- const anchoredBoundary = atSeq === undefined
213
- ? undefined
214
- : source.events.find(event => event.type === 'turn/end' && event.seq >= atSeq);
215
- const boundary = anchoredBoundary
216
- ?? (atSeq === undefined || atSeq > lastSeq
217
- ? source.events.findLast(event => event.type === 'turn/end')
218
- : undefined);
219
- if (boundary === undefined) {
220
- throw new RemoteError('session/fork-unavailable', atSeq !== undefined && atSeq <= lastSeq
221
- ? `session "${request.sessionId}" has not completed the turn containing event ${String(atSeq)}`
222
- : `session "${request.sessionId}" has no completed turn to fork from`, { sessionId: request.sessionId });
231
+ const boundary = atSeq ?? latestCompletedPrefixBoundary(source.events);
232
+ if (boundary === undefined || source.events[boundary]?.seq !== boundary) {
233
+ throw new RemoteError('session/fork-unavailable', request.atSeq === undefined
234
+ ? `session "${request.sessionId}" has no completed turn to fork from`
235
+ : `event ${String(request.atSeq)} does not exist in session "${request.sessionId}" (last seq: ${String(source.events.at(-1)?.seq ?? 'none')})`, { sessionId: request.sessionId });
223
236
  }
224
- const cut = SessionLogOffset(boundary.seq + 1);
237
+ const seed = buildForkSeed(source.events, boundary);
225
238
  let workspace;
226
239
  try {
227
240
  workspace = await this.forkWorkspace(source.header);
@@ -235,8 +248,8 @@ export class SessionCommandController {
235
248
  const { provider, model } = this.ctx.agentDefaultModel.currentSelection();
236
249
  await this.ctx.agents.create({
237
250
  sessionId: childId,
238
- seed: source.events.slice(0, cut),
239
- inheritedEventCount: cut,
251
+ seed,
252
+ inheritedEventCount: SessionLogOffset(boundary + 1),
240
253
  meta: {
241
254
  ...(source.header.cwd === undefined ? {} : { cwd: source.header.cwd }),
242
255
  parentSession: source.header.id,
@@ -383,6 +396,7 @@ export class SessionCommandController {
383
396
  */
384
397
  async updateQueue(request) {
385
398
  if (request.action.kind === 'edit') {
399
+ // oxlint-disable-next-line typescript/no-unnecessary-condition -- Remote callers can submit untyped JSON.
386
400
  if (request.action.content.some(block => block.type !== 'text')) {
387
401
  throw new RemoteError('session/attachment-invalid', 'queue edits accept text content only', { reason: 'QUEUE_EDIT_NON_TEXT' });
388
402
  }
@@ -471,6 +485,9 @@ export class SessionCommandController {
471
485
  rejectCreation(sessionId, error) {
472
486
  if (remoteErrorOf(error) !== undefined)
473
487
  throw error;
488
+ if (error instanceof Error && error.name === 'SessionAlreadyOwnedError') {
489
+ throw new RemoteError('session/writer-held', error.message, { sessionId });
490
+ }
474
491
  if (error instanceof ApiSessionPresetConflict) {
475
492
  throw new RemoteError('agent-preset/conflict', error.message, {
476
493
  sessionId: error.sessionId,
@@ -554,33 +571,52 @@ function imageBlockIn(content, match) {
554
571
  if (match(ref))
555
572
  return ref;
556
573
  }
557
- if (block.type === 'tool-result') {
558
- const nested = imageBlockIn(block.content, match);
559
- if (nested !== undefined)
560
- return nested;
561
- }
562
574
  }
563
575
  return undefined;
564
576
  }
577
+ /** Read only first-party declared content fields; unknown event payloads stay opaque. */
565
578
  function imageInEvent(event, match) {
566
579
  const data = event.data;
567
- const direct = imageBlockIn(data.content, match);
568
- if (direct !== undefined)
569
- return direct;
570
- const message = imageBlockIn(data.message?.content, match);
571
- if (message !== undefined)
572
- return message;
573
- for (const inserted of data.inserted ?? []) {
574
- const found = imageBlockIn(inserted.content, match);
575
- if (found !== undefined)
576
- return found;
577
- }
578
- if (event.type === 'assistant/message' || event.type === 'assistant/attempt') {
579
- for (const chunk of assistantStreamChunks(event.data.stream, 'block-end')) {
580
- const found = imageBlockIn([chunk.block], match);
580
+ // First-party event payloads can be present without their producer plugin mounted.
581
+ const type = event.type;
582
+ switch (type) {
583
+ case 'user/message':
584
+ case 'tool/ptc-dispatch':
585
+ return imageBlockIn(data.content, match);
586
+ case 'system/message':
587
+ case 'developer/message':
588
+ case 'tool/result':
589
+ case 'team/message/queued':
590
+ return imageBlockIn(data.message?.content, match);
591
+ case 'agent/inbox/spliced': {
592
+ const messages = data.inserted;
593
+ if (!Array.isArray(messages))
594
+ return undefined;
595
+ for (const message of messages) {
596
+ if (typeof message !== 'object' || message === null || Array.isArray(message))
597
+ continue;
598
+ const found = imageBlockIn(message.content, match);
599
+ if (found !== undefined)
600
+ return found;
601
+ }
602
+ return undefined;
603
+ }
604
+ case 'compaction/summary':
605
+ return imageBlockIn(data.summary, match) ?? imageBlockIn(data.rawOutput, match);
606
+ case 'assistant/message': {
607
+ const found = imageBlockIn(data.message?.content, match);
581
608
  if (found !== undefined)
582
609
  return found;
610
+ break;
583
611
  }
612
+ case 'assistant/attempt': break;
613
+ default: return undefined;
614
+ }
615
+ const assistant = event;
616
+ for (const chunk of assistantStreamChunks(assistant.data.stream, 'block-end')) {
617
+ const found = imageBlockIn([chunk.block], match);
618
+ if (found !== undefined)
619
+ return found;
584
620
  }
585
621
  return undefined;
586
622
  }
@@ -1,11 +1,11 @@
1
- /** Live Session jobs and projection state with reconnect baselines. */
1
+ /** Live Session projection state with reconnect baselines. */
2
2
  import type { Context } from '@deepseek-ai/cordis';
3
3
  import type { SessionControlFrame } from './types.ts';
4
4
  /** Owns the Host-wide Session control stream. */
5
5
  export declare class SessionControlController {
6
6
  private readonly ctx;
7
7
  private readonly streams;
8
- /** @param ctx - Host context carrying live Agent, projection, and jobs services. */
8
+ /** @param ctx - Host context carrying live Agent and projection services. */
9
9
  constructor(ctx: Context);
10
10
  /**
11
11
  * Open one generation of Host-wide live control state.
@@ -15,8 +15,6 @@ export declare class SessionControlController {
15
15
  control(signal: AbortSignal): AsyncIterable<SessionControlFrame>;
16
16
  private baseline;
17
17
  private projectionBaseline;
18
- private onJobsChanged;
19
- private jobsFor;
20
18
  private broadcast;
21
19
  }
22
20
  //# sourceMappingURL=control.d.ts.map
@@ -1,10 +1,10 @@
1
- /** Live Session jobs and projection state with reconnect baselines. */
1
+ /** Live Session projection state with reconnect baselines. */
2
2
  import { Deque } from '@deepseek-ai/dsh-deque';
3
3
  /** Owns the Host-wide Session control stream. */
4
4
  export class SessionControlController {
5
5
  ctx;
6
6
  streams = new Set();
7
- /** @param ctx - Host context carrying live Agent, projection, and jobs services. */
7
+ /** @param ctx - Host context carrying live Agent and projection services. */
8
8
  constructor(ctx) {
9
9
  this.ctx = ctx;
10
10
  ctx.sessionProjections.onChanged((session, key, value, seq) => {
@@ -16,14 +16,6 @@ export class SessionControlController {
16
16
  seq,
17
17
  });
18
18
  });
19
- ctx.inject(['jobs'], (jobsCtx) => {
20
- jobsCtx.jobs.onJobsChanged((owner) => { this.onJobsChanged(owner); });
21
- });
22
- ctx.on('session/created', (session) => {
23
- const jobs = this.jobsFor(this.ctx.agents.get(session.id));
24
- if (jobs.length > 0)
25
- this.broadcast({ type: 'jobs', sessionId: session.id, jobs });
26
- });
27
19
  ctx.effect(() => () => {
28
20
  for (const stream of this.streams)
29
21
  stream.end();
@@ -50,13 +42,7 @@ export class SessionControlController {
50
42
  }
51
43
  baseline() {
52
44
  const sessions = this.ctx.sessions.list();
53
- const jobs = Object.create(null);
54
- for (const session of sessions) {
55
- const agent = this.ctx.agents.get(session.id);
56
- jobs[session.id] = this.jobsFor(agent);
57
- }
58
45
  return {
59
- jobs,
60
46
  projections: this.projectionBaseline(sessions),
61
47
  };
62
48
  }
@@ -72,23 +58,6 @@ export class SessionControlController {
72
58
  }
73
59
  return blocks;
74
60
  }
75
- onJobsChanged(owner) {
76
- if (owner !== undefined) {
77
- this.broadcast({ type: 'jobs', sessionId: owner.id, jobs: this.jobsFor(owner) });
78
- return;
79
- }
80
- for (const session of this.ctx.sessions.list()) {
81
- this.broadcast({
82
- type: 'jobs',
83
- sessionId: session.id,
84
- jobs: this.jobsFor(this.ctx.agents.get(session.id)),
85
- });
86
- }
87
- }
88
- jobsFor(agent) {
89
- const jobs = this.ctx.get('jobs');
90
- return jobs === undefined ? [] : jobs.list(agent).map(jobView);
91
- }
92
61
  broadcast(frame) {
93
62
  for (const stream of this.streams)
94
63
  stream.push(frame);
@@ -135,15 +104,4 @@ class ControlQueue {
135
104
  }
136
105
  }
137
106
  }
138
- function jobView(job) {
139
- return {
140
- id: job.id,
141
- kind: job.kind,
142
- label: job.label,
143
- status: job.status,
144
- ...(job.detail === undefined ? {} : { detail: job.detail }),
145
- startedAt: job.startedAt,
146
- ...(job.finishedAt === undefined ? {} : { finishedAt: job.finishedAt }),
147
- };
148
- }
149
107
  //# sourceMappingURL=control.js.map
@@ -370,7 +370,7 @@ function validateAddress(address, header, inheritedEventCount, projections) {
370
370
  reason: 'unsupported',
371
371
  });
372
372
  }
373
- if (identity.mode !== address.mode) {
373
+ if (address.mode !== 'unknown' && identity.mode !== address.mode) {
374
374
  throw new RemoteError('subagent/unauthorized', 'subagent mode does not match the supplied address', {
375
375
  childSessionId: address.childSessionId,
376
376
  });