@amaster.ai/dsh-a2a 0.1.36 → 0.1.37

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.
package/README.md CHANGED
@@ -20,6 +20,8 @@ Standard `SendMessage` / `SendStreamingMessage`, task queries, cancellation and
20
20
 
21
21
  The same-process `a2aInbox` service exposes `resolve`, `content`, `enqueue`, `promote`, `cancel`, `get`, `update`, `move`, `remove`, `clearPending`, and `executePending`, plus the existing pause/wake/resume controls. Pending-only clearing preserves active work; context clearing discards all work.
22
22
 
23
+ Pause is process-local and writes no custom required session event. After restart, restored input stays parked until a waking admission or explicit execution. Retention recovery accepts snapshots under the historical extension URI as well as the current URI, writes only the current metadata key, and clears both keys after consumption. Explicit resume durably transfers input to a fresh task before waking it; retry after an interrupted cleanup reuses the existing replacement instead of replaying its input.
24
+
23
25
  For external clients, the [native inbox extension specification](#native-inbox-extension-v1) defines `inbox/get`, `update`, `move`, `remove`, `clear`, `execute`, `steer`, and `subscribe` at the advertised JSONRPC endpoint. Activate it with the `A2A-Extensions` header set to the extension URI. Updates use standard A2A 1.0 user Messages; operations address native inbox IDs. REST/gRPC clients use the advertised JSONRPC endpoint for these extension methods. No extra queue or consumer is created.
24
26
 
25
27
  `inbox/subscribe` streams checkpointed invalidations `{contextId, version}` over SSE, including native consumption and edits. It registers before its initial checkpoint and sends only after owner-scoped `ctx.sessions.flush()` returns true. A false/rejected checkpoint sends no success notification. Re-read the live inbox on invalidation and reconnect; there is no replay or durable outbox. Slow clients disconnect instead of accumulating notifications. To publish the same invalidations to host listeners on `a2a/inbox-checkpointed` without an SSE subscriber, enable `inbox.notifications: true` (default false). Hosts retain authentication, authorization and product-specific fan-out.
package/lib/inbox.js CHANGED
@@ -9,6 +9,16 @@ import { commitInboxInput, markInboxInputPending } from './task-store.js';
9
9
  import { agentTextMessage, SessionTranslator, terminalStatusUpdate } from './translator.js';
10
10
  export const INBOX_EXTENSION = 'https://github.com/TGYD-helige/dsh-plugins/blob/main/packages/dsh-a2a/README.md';
11
11
  const RETAINED_INPUTS = `${INBOX_EXTENSION}#retained-inputs`;
12
+ const LEGACY_RETAINED_INPUTS = 'https://github.com/TGYD-helige/dsh-plugins/blob/main/docs/protocol/inbox-v1.md#retained-inputs';
13
+ function retainedSnapshot(task) {
14
+ return task.metadata?.[RETAINED_INPUTS] ?? task.metadata?.[LEGACY_RETAINED_INPUTS];
15
+ }
16
+ function clearRetainedSnapshot(task) {
17
+ if (!task.metadata)
18
+ return;
19
+ delete task.metadata[RETAINED_INPUTS];
20
+ delete task.metadata[LEGACY_RETAINED_INPUTS];
21
+ }
12
22
  export function validateInboxPage(page) {
13
23
  if (page === undefined)
14
24
  return;
@@ -508,56 +518,103 @@ export class NativeInbox {
508
518
  }
509
519
  async resume(contextId, taskId) {
510
520
  const context = new ServerCallContext();
511
- const previous = await this.store.load(taskId, context);
512
- if (!previous || previous.contextId !== contextId || !this.terminal(previous))
513
- throw new Error('Resume requires a terminal retained task.');
514
- const retained = previous.metadata?.[RETAINED_INPUTS];
515
- if (typeof retained !== 'string')
516
- throw new Error('Task has no retained input to resume.');
517
- const inputs = JSON.parse(retained);
518
- if (!inputs.length)
519
- throw new Error('Task has no retained input to resume.');
520
521
  const run = await this.context(contextId, context);
521
- const id = randomUUID();
522
- const replacement = {
523
- ...previous,
524
- id,
525
- status: {
526
- state: TaskState.TASK_STATE_SUBMITTED,
527
- timestamp: new Date().toISOString(),
528
- message: undefined,
529
- },
530
- history: [],
531
- metadata: { ...previous.metadata },
532
- };
533
- delete replacement.metadata[RETAINED_INPUTS];
534
- const retainedInputs = inputs.map((input) => {
535
- if (input.message.source.kind !== 'a2a')
536
- throw new Error('Retained input has invalid task ownership.');
537
- const original = Message.fromJSON(JSON.parse(input.message.source.message));
538
- original.taskId = id;
539
- original.referenceTaskIds = [...new Set([...original.referenceTaskIds, taskId])];
540
- replacement.history.push(markInboxInputPending(original));
541
- return {
542
- ...input,
543
- message: {
544
- ...input.message,
545
- source: {
546
- ...input.message.source,
547
- taskId: id,
548
- message: JSON.stringify(Message.toJSON(original)),
549
- },
522
+ const operation = run.admissionTail.then(async () => {
523
+ if (this.stopping || this.bridge.isClearing(contextId))
524
+ throw new Error('Context is unavailable.');
525
+ const previous = await this.store.load(taskId, context);
526
+ if (!previous || previous.contextId !== contextId || !this.terminal(previous))
527
+ throw new Error('Resume requires a terminal retained task.');
528
+ const retained = retainedSnapshot(previous);
529
+ if (typeof retained !== 'string')
530
+ throw new Error('Task has no retained input to resume.');
531
+ const inputs = JSON.parse(retained);
532
+ if (!inputs.length)
533
+ throw new Error('Task has no retained input to resume.');
534
+ const nativeIds = new Set(inputs.map((input) => input.message.id));
535
+ const resumed = new Map();
536
+ const messages = [
537
+ ...run.entry.handle.agent.session
538
+ .snapshotEvents()
539
+ .flatMap((event) => (event.type === 'user/message' ? [event.data] : [])),
540
+ ...this.pending(run),
541
+ ...[...run.tasks.values()].flatMap((task) => task.claimedInputs ?? []),
542
+ ];
543
+ for (const message of messages) {
544
+ if (!nativeIds.has(message.id) || message.source.kind !== 'a2a')
545
+ continue;
546
+ const original = Message.fromJSON(JSON.parse(message.source.message));
547
+ if (message.source.taskId !== taskId && original.referenceTaskIds.includes(taskId))
548
+ resumed.set(message.id, message.source.taskId);
549
+ }
550
+ if (resumed.size) {
551
+ const replacements = new Set(resumed.values());
552
+ if (resumed.size !== nativeIds.size || replacements.size !== 1)
553
+ throw new Error('Retained input has already been partially resumed.');
554
+ const id = resumed.values().next().value;
555
+ await this.flush(run.entry.handle.agent.session);
556
+ const replacement = run.tasks.get(id)?.task ?? (await this.store.load(id, context));
557
+ if (!replacement)
558
+ throw new Error('Resumed task is unavailable.');
559
+ clearRetainedSnapshot(replacement);
560
+ await this.checkpoint(replacement, context);
561
+ clearRetainedSnapshot(previous);
562
+ await this.checkpoint(previous, context);
563
+ if (this.pending(run).some((message) => message.source.kind === 'a2a' && message.source.taskId === id))
564
+ await this.wake(contextId);
565
+ return id;
566
+ }
567
+ const id = randomUUID();
568
+ const replacement = {
569
+ ...previous,
570
+ id,
571
+ status: {
572
+ state: TaskState.TASK_STATE_SUBMITTED,
573
+ timestamp: new Date().toISOString(),
574
+ message: undefined,
550
575
  },
576
+ history: [],
577
+ metadata: { ...previous.metadata },
551
578
  };
579
+ clearRetainedSnapshot(replacement);
580
+ const retainedInputs = inputs.map((input) => {
581
+ if (input.message.source.kind !== 'a2a')
582
+ throw new Error('Retained input has invalid task ownership.');
583
+ const original = Message.fromJSON(JSON.parse(input.message.source.message));
584
+ original.taskId = id;
585
+ original.referenceTaskIds = [...new Set([...original.referenceTaskIds, taskId])];
586
+ replacement.history.push(markInboxInputPending(original));
587
+ return {
588
+ ...input,
589
+ message: {
590
+ ...input.message,
591
+ source: {
592
+ ...input.message.source,
593
+ taskId: id,
594
+ message: JSON.stringify(Message.toJSON(original)),
595
+ },
596
+ },
597
+ };
598
+ });
599
+ replacement.metadata[RETAINED_INPUTS] = JSON.stringify(retainedInputs);
600
+ await this.checkpoint(replacement, context);
601
+ for (const input of retainedInputs) {
602
+ if (this.pending(run).some((pending) => pending.id === input.message.id))
603
+ run.entry.handle.agent.inbox.remove(input.message.id);
604
+ run.entry.handle.agent.send(input.message, input.target, false);
605
+ }
606
+ this.restoreTask(run, replacement, context);
607
+ // Consume the old snapshot only after its replacement input is durable.
608
+ await this.flush(run.entry.handle.agent.session);
609
+ clearRetainedSnapshot(replacement);
610
+ await this.checkpoint(replacement, context);
611
+ clearRetainedSnapshot(previous);
612
+ await this.checkpoint(previous, context);
613
+ await this.wake(contextId);
614
+ return id;
552
615
  });
553
- await this.checkpoint(replacement, context);
554
- for (const [index, input] of retainedInputs.entries()) {
555
- if (this.pending(run).some((pending) => pending.id === input.message.id))
556
- run.entry.handle.agent.inbox.remove(input.message.id);
557
- run.entry.handle.agent.send(input.message, input.target, index === retainedInputs.length - 1);
558
- }
559
- this.restoreTask(run, replacement, context);
560
- return id;
616
+ run.admissionTail = operation.then(() => { }, () => { });
617
+ return operation;
561
618
  }
562
619
  /** Repair only an incomplete ownership move, never an ordinary DSH claim. */
563
620
  async recoverPromotions(run, context) {
@@ -632,7 +689,7 @@ export class NativeInbox {
632
689
  statusTimestampAfter: undefined,
633
690
  }, context);
634
691
  for (const task of page.tasks) {
635
- const retained = task.metadata?.[RETAINED_INPUTS];
692
+ const retained = retainedSnapshot(task);
636
693
  if (typeof retained !== 'string')
637
694
  continue;
638
695
  // Terminal snapshots belong to explicit resume(); do not consume their
@@ -736,7 +793,7 @@ export class NativeInbox {
736
793
  return;
737
794
  await this.flush(agent.session);
738
795
  for (const task of snapshots) {
739
- delete task.metadata[RETAINED_INPUTS];
796
+ clearRetainedSnapshot(task);
740
797
  await this.checkpoint(task, context);
741
798
  }
742
799
  }
@@ -762,11 +819,6 @@ export class NativeInbox {
762
819
  rolledBack: new Set(),
763
820
  retained: { 'next-step': [], 'next-turn': [] },
764
821
  };
765
- const pause = [...run.entry.handle.agent.session.snapshotEvents()]
766
- .reverse()
767
- .find((event) => event.type === 'agent/inbox/paused');
768
- if (pause)
769
- run.paused = pause.data.paused;
770
822
  await this.recoverRetained(run, context);
771
823
  await this.recoverPromotions(run, context);
772
824
  const snapshots = new Map();
@@ -1548,7 +1600,6 @@ export class NativeInbox {
1548
1600
  if (run.paused)
1549
1601
  return;
1550
1602
  run.paused = true;
1551
- run.entry.handle.agent.session.append('agent/inbox/paused', { paused: true });
1552
1603
  for (const admission of run.admissions)
1553
1604
  admission.abort();
1554
1605
  run.entry.handle.agent.cancel({ kind: 'user' }, { keepInbox: true });
@@ -1560,7 +1611,6 @@ export class NativeInbox {
1560
1611
  const agent = run.entry.handle.agent;
1561
1612
  const pending = this.pending(run);
1562
1613
  run.paused = false;
1563
- agent.session.append('agent/inbox/paused', { paused: false });
1564
1614
  const target = agent.inbox.nextTurn.length ? 'next-turn' : 'next-step';
1565
1615
  const items = target === 'next-turn' ? agent.inbox.nextTurn : agent.inbox.nextStep;
1566
1616
  const input = items.at(-1);
@@ -1732,6 +1782,7 @@ export class NativeInbox {
1732
1782
  if (this.retainPending) {
1733
1783
  const pending = retainedByTask.get(task.task.id) ?? [];
1734
1784
  if (pending.length) {
1785
+ clearRetainedSnapshot(task.task);
1735
1786
  task.task.metadata = {
1736
1787
  ...task.task.metadata,
1737
1788
  [RETAINED_INPUTS]: JSON.stringify(pending),
@@ -46,13 +46,6 @@ declare module '@deepseek-ai/dsh-llm/types' {
46
46
  goal: GoalMessageSource;
47
47
  }
48
48
  }
49
- declare module '@deepseek-ai/dsh-session/types' {
50
- interface SessionEventMap {
51
- 'agent/inbox/paused': {
52
- paused: boolean;
53
- };
54
- }
55
- }
56
49
  declare module '@deepseek-ai/cordis' {
57
50
  interface Events {
58
51
  /** Native inbox lifecycle; content consumers must honor their capture settings. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amaster.ai/dsh-a2a",
3
- "version": "0.1.36",
3
+ "version": "0.1.37",
4
4
  "description": "A2A protocol (JSON-RPC + SSE) server plugin for DeepSeek Harness: expose dsh agents as A2A agents.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",