@gajae-code/coding-agent 0.16.1 → 0.16.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/dist/types/cli/args.d.ts +2 -2
  3. package/dist/types/cli/root-flags.d.ts +4 -2
  4. package/dist/types/cli.d.ts +1 -1
  5. package/dist/types/commands/launch.d.ts +1 -1
  6. package/dist/types/config/autorouting-tier-map.d.ts +3 -0
  7. package/dist/types/config/model-registry.d.ts +4 -0
  8. package/dist/types/config/model-resolver.d.ts +9 -0
  9. package/dist/types/config/settings-schema.d.ts +29 -1
  10. package/dist/types/discovery/claude-plugins.d.ts +3 -1
  11. package/dist/types/discovery/helpers.d.ts +2 -0
  12. package/dist/types/extensibility/gjc-plugins/mcp-policy.d.ts +16 -0
  13. package/dist/types/extensibility/gjc-plugins/paths.d.ts +2 -0
  14. package/dist/types/extensibility/runtime-skill-discovery.d.ts +1 -1
  15. package/dist/types/extensibility/shared-events.d.ts +1 -1
  16. package/dist/types/gjc-runtime/session-state-lock.d.ts +2 -0
  17. package/dist/types/modes/components/assistant-message.d.ts +1 -2
  18. package/dist/types/modes/components/settings-selector.d.ts +6 -1
  19. package/dist/types/modes/components/status-line/command.d.ts +35 -0
  20. package/dist/types/modes/components/status-line/index.d.ts +1 -0
  21. package/dist/types/modes/components/status-line/types.d.ts +5 -0
  22. package/dist/types/modes/components/tool-status-header.d.ts +14 -0
  23. package/dist/types/modes/controllers/event-controller.d.ts +6 -0
  24. package/dist/types/modes/interactive-mode.d.ts +1 -0
  25. package/dist/types/modes/types.d.ts +1 -0
  26. package/dist/types/runtime-mcp/client.d.ts +9 -1
  27. package/dist/types/runtime-mcp/manager.d.ts +7 -1
  28. package/dist/types/runtime-mcp/pool.d.ts +22 -3
  29. package/dist/types/runtime-mcp/transports/stdio.d.ts +0 -4
  30. package/dist/types/runtime-mcp/types.d.ts +22 -0
  31. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +12 -0
  32. package/dist/types/sdk/bus/slack-live-provider.d.ts +6 -0
  33. package/dist/types/sdk/bus/slack-provider.d.ts +12 -0
  34. package/dist/types/sdk/cli/rows.d.ts +7 -0
  35. package/dist/types/sdk/host/events.d.ts +1 -0
  36. package/dist/types/sdk/host/host.d.ts +2 -0
  37. package/dist/types/sdk/host/session-runtime.d.ts +23 -0
  38. package/dist/types/sdk/session-api-key.d.ts +26 -0
  39. package/dist/types/sdk/transport/serve-cli.d.ts +1 -1
  40. package/dist/types/session/agent-session.d.ts +9 -0
  41. package/dist/types/setup/paseo/announce.d.ts +111 -0
  42. package/dist/types/setup/paseo/setup-deps.d.ts +2 -1
  43. package/dist/types/system-prompt.d.ts +2 -0
  44. package/dist/types/task/executor.d.ts +2 -1
  45. package/dist/types/tools/fetch.d.ts +7 -0
  46. package/dist/types/tools/index.d.ts +8 -2
  47. package/dist/types/tools/path-utils.d.ts +2 -0
  48. package/dist/types/tools/search.d.ts +6 -0
  49. package/dist/types/utils/external-editor.d.ts +5 -0
  50. package/package.json +7 -7
  51. package/scripts/capture-status-line-custom-editor-showcase.ts +151 -0
  52. package/scripts/generate-sdk-operation-inventory.ts +9 -0
  53. package/scripts/verify-status-line-custom-editor-showcase.ts +309 -0
  54. package/src/cli/args.ts +20 -9
  55. package/src/cli/root-flags.ts +5 -3
  56. package/src/config/autorouting-tier-map.ts +1 -0
  57. package/src/config/file-lock.ts +181 -17
  58. package/src/config/model-preset-registry.ts +5 -3
  59. package/src/config/model-profiles.ts +40 -0
  60. package/src/config/model-registry.ts +57 -11
  61. package/src/config/model-resolver.ts +48 -0
  62. package/src/config/settings-schema.ts +33 -1
  63. package/src/config/settings.ts +1 -0
  64. package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +11 -11
  65. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +13 -2
  66. package/src/discovery/builtin.ts +13 -4
  67. package/src/discovery/claude-plugins.ts +12 -4
  68. package/src/discovery/helpers.ts +10 -2
  69. package/src/extensibility/gjc-plugins/compiler.ts +208 -58
  70. package/src/extensibility/gjc-plugins/installer.ts +13 -12
  71. package/src/extensibility/gjc-plugins/mcp-policy.ts +81 -52
  72. package/src/extensibility/gjc-plugins/paths.ts +13 -0
  73. package/src/extensibility/gjc-plugins/runtime-adapters.ts +758 -16
  74. package/src/extensibility/runtime-skill-discovery.ts +77 -151
  75. package/src/extensibility/shared-events.ts +1 -1
  76. package/src/extensibility/skills.ts +44 -16
  77. package/src/gjc-runtime/repository-binding.ts +1 -1
  78. package/src/gjc-runtime/session-state-lock.ts +94 -6
  79. package/src/internal-urls/docs-index.generated.ts +10 -10
  80. package/src/main.ts +72 -34
  81. package/src/modes/DESIGN.md +54 -0
  82. package/src/modes/acp/acp-agent.ts +29 -14
  83. package/src/modes/components/assistant-message.ts +22 -9
  84. package/src/modes/components/model-selector.ts +166 -33
  85. package/src/modes/components/settings-selector.ts +1075 -235
  86. package/src/modes/components/status-line/command.ts +190 -0
  87. package/src/modes/components/status-line/index.ts +1 -0
  88. package/src/modes/components/status-line/segments.ts +32 -5
  89. package/src/modes/components/status-line/types.ts +5 -0
  90. package/src/modes/components/tool-status-header.ts +393 -21
  91. package/src/modes/controllers/command-controller.ts +2 -0
  92. package/src/modes/controllers/event-controller.ts +148 -5
  93. package/src/modes/controllers/selector-controller.ts +21 -0
  94. package/src/modes/interactive-mode.ts +47 -1
  95. package/src/modes/types.ts +1 -0
  96. package/src/modes/utils/ui-helpers.ts +1 -0
  97. package/src/prompts/system/system-prompt.md +8 -0
  98. package/src/prompts/tools/bash.md +1 -1
  99. package/src/prompts/tools/job.md +1 -1
  100. package/src/prompts/tools/search.md +2 -0
  101. package/src/runtime-mcp/client.ts +60 -17
  102. package/src/runtime-mcp/manager.ts +171 -14
  103. package/src/runtime-mcp/pool.test.ts +209 -0
  104. package/src/runtime-mcp/pool.ts +224 -23
  105. package/src/runtime-mcp/transports/stdio.ts +158 -24
  106. package/src/runtime-mcp/types.ts +24 -0
  107. package/src/sdk/acp/adapter.ts +1 -1
  108. package/src/sdk/broker/broker.ts +5 -4
  109. package/src/sdk/bus/chat-daemon-control.ts +1 -1
  110. package/src/sdk/bus/chat-daemon-runtime.ts +44 -1
  111. package/src/sdk/bus/slack-daemon.ts +92 -2
  112. package/src/sdk/bus/slack-live-provider.ts +12 -0
  113. package/src/sdk/bus/slack-provider.ts +5 -0
  114. package/src/sdk/cli/rows.ts +47 -2
  115. package/src/sdk/cli/session-cli.ts +47 -23
  116. package/src/sdk/host/events.ts +25 -2
  117. package/src/sdk/host/host.ts +4 -1
  118. package/src/sdk/host/session-runtime.test.ts +36 -25
  119. package/src/sdk/host/session-runtime.ts +279 -43
  120. package/src/sdk/protocol/operation-inventory.generated.json +66 -0
  121. package/src/sdk/router/session-router.ts +60 -37
  122. package/src/sdk/session-api-key.ts +94 -0
  123. package/src/sdk/session.ts +73 -30
  124. package/src/sdk/transport/serve-cli.ts +3 -3
  125. package/src/session/agent-session.ts +123 -38
  126. package/src/session/contribution-prep.ts +214 -21
  127. package/src/setup/paseo/announce.ts +450 -0
  128. package/src/setup/paseo/setup-deps.ts +15 -3
  129. package/src/slash-commands/builtin-registry.ts +1 -0
  130. package/src/system-prompt.ts +4 -0
  131. package/src/task/executor.ts +2 -1
  132. package/src/task/index.ts +17 -4
  133. package/src/tools/bash.ts +50 -21
  134. package/src/tools/cron.ts +1 -1
  135. package/src/tools/fetch.ts +29 -1
  136. package/src/tools/index.ts +8 -2
  137. package/src/tools/job.ts +8 -2
  138. package/src/tools/monitor.ts +8 -2
  139. package/src/tools/path-utils.ts +7 -0
  140. package/src/tools/search.ts +116 -58
  141. package/src/tools/skill.ts +22 -2
  142. package/src/tools/subagent.ts +3 -1
  143. package/src/tools/tool-catalog.generated.ts +9 -2
  144. package/src/utils/external-editor.ts +16 -2
  145. package/src/web/insane/bridge.ts +113 -23
  146. package/src/web/search/providers/perplexity.ts +102 -41
@@ -136,7 +136,7 @@ export type ChatDaemonAction = "stop" | "reload";
136
136
  */
137
137
  export const CHAT_DAEMON_GENERATIONS: Readonly<Record<ChatDaemonKind, number>> = {
138
138
  discord: 73,
139
- slack: 77,
139
+ slack: 79,
140
140
  };
141
141
 
142
142
  export function chatDaemonGeneration(kind: ChatDaemonKind): number {
@@ -92,6 +92,36 @@ function isReservedIdentity(name: string | undefined): boolean {
92
92
  return isLifecycleEvent(name) || isControlPlaneFrameType(name);
93
93
  }
94
94
 
95
+ /**
96
+ * Chat notifications in lean mode are an answer surface, not a mirror of the
97
+ * session protocol. Lifecycle, identity, and activity frames remain available
98
+ * to verbose consumers, while finalized answers and action-needed events are
99
+ * delivered in lean mode.
100
+ */
101
+ export function shouldPublishChatFrame(
102
+ frame: Record<string, unknown>,
103
+ verbosity: "lean" | "verbose" | undefined,
104
+ ): boolean {
105
+ if (verbosity !== "lean") return frame.type !== "turn_stream" || frame.phase !== "live";
106
+ return frame.type === "turn_stream" && frame.phase === "finalized" && frame.finalAnswer === true;
107
+ }
108
+
109
+ /**
110
+ * A final answer's message ref identifies its one rendered message even when
111
+ * the SDK transport delivers an unpositioned duplicate frame.
112
+ */
113
+ export function publicationIdForFinalChatAnswer(sessionId: string, frame: Record<string, unknown>): string | undefined {
114
+ if (
115
+ frame.type !== "turn_stream" ||
116
+ frame.phase !== "finalized" ||
117
+ frame.finalAnswer !== true ||
118
+ typeof frame.messageRef !== "string" ||
119
+ frame.messageRef.length === 0
120
+ )
121
+ return undefined;
122
+ return `turn:${sessionId}:${frame.messageRef}`;
123
+ }
124
+
95
125
  /** One delivered frame reduced to a single event identity. */
96
126
  type CorrelatedFrame = SessionRouterFrame;
97
127
 
@@ -471,7 +501,14 @@ export class ChatDaemonRuntime {
471
501
  }
472
502
 
473
503
  async #onAttachment(attachment: SessionAttachment): Promise<void> {
504
+ const revivedTransport = this.#attachments.get(attachment.sessionId) === attachment;
474
505
  this.#attachments.set(attachment.sessionId, attachment);
506
+ if (revivedTransport) {
507
+ this.#presentation?.connectSession(attachment.sessionId, {
508
+ sendReply: route => attachment.send({ type: "reply", id: route.actionId, answer: route.answer }),
509
+ });
510
+ return;
511
+ }
475
512
  const discord = this.#discord;
476
513
  const slack = this.#slack;
477
514
  const barrier = (async () => {
@@ -564,8 +601,9 @@ export class ChatDaemonRuntime {
564
601
  if (this.#attachments.get(attachment.sessionId) !== attachment) return;
565
602
  await this.#attachmentBarriers.get(attachment.sessionId);
566
603
  if (this.#attachments.get(attachment.sessionId) !== attachment) return;
567
- const publicationId = correlated.publicationId;
568
604
  const normalizedFrame = correlated.body;
605
+ const publicationId =
606
+ publicationIdForFinalChatAnswer(attachment.sessionId, normalizedFrame) ?? correlated.publicationId;
569
607
  const bodyType = typeof normalizedFrame.type === "string" ? normalizedFrame.type : undefined;
570
608
  if (isControlPlaneFrameType(correlated.name) || isControlPlaneFrameType(bodyType)) return;
571
609
  if (normalizedFrame.type === "turn_stream" && normalizedFrame.phase === "live") return;
@@ -600,6 +638,11 @@ export class ChatDaemonRuntime {
600
638
  return;
601
639
  }
602
640
  if (!notification) return;
641
+ if (
642
+ notification.type === "frame" &&
643
+ !shouldPublishChatFrame(normalizedFrame, this.input.config.presentation?.verbosity)
644
+ )
645
+ return;
603
646
  const payload = this.#presentation?.fanout(notification)[0];
604
647
  const body = payload?.body;
605
648
  const content =
@@ -58,7 +58,9 @@ function slackPublicationClientMsgId(publicationId: string): string {
58
58
 
59
59
  // Durable filesystem publication leases must outlast one event-loop and persistence turn.
60
60
  const MIN_PUBLICATION_LEASE_MS = 100;
61
+ const INBOUND_REACTION_DEADLINE_MS = 5_000;
61
62
  const RETIREMENT_DRAIN_MS = 5_000;
63
+ const INBOUND_REACTION = "eyes";
62
64
 
63
65
  type SlackProviderWorkScope = {
64
66
  sessionId: string;
@@ -70,6 +72,8 @@ type SlackProviderWork = {
70
72
  scopeKey: string;
71
73
  invalidate(): Promise<void>;
72
74
  abort(): void;
75
+ /** Best-effort work is aborted before the graceful shutdown drain. */
76
+ abortOnStop?: boolean;
73
77
  terminalize(): Promise<void>;
74
78
  settled: Promise<void>;
75
79
  };
@@ -362,6 +366,7 @@ export class SlackNotificationDaemon {
362
366
  this.#startedGeneration = undefined;
363
367
  if (this.#providerRunningGeneration === lifecycleGeneration) this.#providerRunningGeneration = undefined;
364
368
  this.#clearLeaseRecoveryTimer();
369
+ for (const work of this.#workInvalidators) if (work.abortOnStop) work.abort();
365
370
  if (stopProvider) this.#providerStopRequestedGenerations.add(lifecycleGeneration);
366
371
  const providerStop = stopProvider ? Promise.resolve().then(() => this.options.provider.stop()) : undefined;
367
372
  const providerStopResult = providerStop?.then(
@@ -449,6 +454,7 @@ export class SlackNotificationDaemon {
449
454
 
450
455
  /** Accepted inbound effects are durably claimed before their Socket Mode ACK. */
451
456
  async handleEnvelope(envelope: SlackSocketEnvelope): Promise<boolean> {
457
+ const lifecycleGeneration = this.#lifecycleGeneration;
452
458
  if (!text(envelope.envelope_id)) return false;
453
459
  const inbound = messageFromEnvelope(envelope.payload);
454
460
  if (!inbound || inbound.teamId !== this.options.teamId || inbound.channelId !== this.options.channelId) {
@@ -479,6 +485,15 @@ export class SlackNotificationDaemon {
479
485
  this.#inflightInbound.add(inflightKey);
480
486
  try {
481
487
  await this.options.provider.ack(envelope.envelope_id);
488
+ const messageTs = text(inbound.event.ts);
489
+ if (messageTs)
490
+ this.#scheduleInboundReaction(
491
+ claim.sessionId,
492
+ claim.endpoint,
493
+ inbound.channelId,
494
+ messageTs,
495
+ lifecycleGeneration,
496
+ );
482
497
  return await this.#dispatchInbound(claim);
483
498
  } finally {
484
499
  this.#inflightInbound.delete(inflightKey);
@@ -491,6 +506,76 @@ export class SlackNotificationDaemon {
491
506
  return work.finally(() => this.#activeWork.delete(work));
492
507
  }
493
508
 
509
+ /** Track the best-effort acknowledgement in the owning attachment's provider-work scope. */
510
+ #scheduleInboundReaction(
511
+ sessionId: string,
512
+ endpoint: SessionAttachment,
513
+ channel: string,
514
+ timestamp: string,
515
+ lifecycleGeneration: number,
516
+ ): void {
517
+ const scope: SlackProviderWorkScope = {
518
+ sessionId,
519
+ endpointGeneration: endpoint.generation,
520
+ ...(endpoint.authorityId === undefined ? {} : { attachmentAuthorityId: endpoint.authorityId }),
521
+ };
522
+ const scopeKey = slackProviderWorkScopeKey(scope);
523
+ const workGeneration = this.#workGeneration;
524
+ const controller = new AbortController();
525
+ const settled = Promise.withResolvers<void>();
526
+ let abortKind: "deadline" | "lifecycle" | undefined;
527
+ const abort = (kind: "deadline" | "lifecycle"): void => {
528
+ abortKind ??= kind;
529
+ controller.abort();
530
+ };
531
+ const work: SlackProviderWork = {
532
+ scopeKey,
533
+ invalidate: async () => abort("lifecycle"),
534
+ abort: () => abort("lifecycle"),
535
+ abortOnStop: true,
536
+ terminalize: async () => abort("lifecycle"),
537
+ settled: settled.promise,
538
+ };
539
+ this.#workInvalidators.add(work);
540
+ const reaction = (async () => {
541
+ const deadline = setTimeout(() => abort("deadline"), INBOUND_REACTION_DEADLINE_MS);
542
+ try {
543
+ if (
544
+ workGeneration !== this.#workGeneration ||
545
+ lifecycleGeneration !== this.#lifecycleGeneration ||
546
+ this.#retiredProviderWorkScopes.has(scopeKey) ||
547
+ !endpoint.isCurrent()
548
+ ) {
549
+ abort("lifecycle");
550
+ return;
551
+ }
552
+ await this.options.provider.addReaction({
553
+ channel,
554
+ timestamp,
555
+ name: INBOUND_REACTION,
556
+ signal: controller.signal,
557
+ });
558
+ if (abortKind === "deadline")
559
+ logger.warn(
560
+ `Slack inbound acknowledgement reaction exceeded the ${INBOUND_REACTION_DEADLINE_MS}ms deadline and was aborted.`,
561
+ );
562
+ } catch {
563
+ if (abortKind !== "lifecycle")
564
+ logger.warn(
565
+ abortKind === "deadline"
566
+ ? `Slack inbound acknowledgement reaction exceeded the ${INBOUND_REACTION_DEADLINE_MS}ms deadline and was aborted.`
567
+ : "Slack inbound acknowledgement reaction failed.",
568
+ );
569
+ } finally {
570
+ clearTimeout(deadline);
571
+ this.#workInvalidators.delete(work);
572
+ settled.resolve();
573
+ this.#tryReleaseRetiredProviderWorkScope(scopeKey);
574
+ }
575
+ })();
576
+ void this.#track(reaction);
577
+ }
578
+
494
579
  async #invalidateActiveWork(): Promise<void> {
495
580
  this.#workGeneration += 1;
496
581
  await Promise.allSettled([...this.#workInvalidators].map(work => work.invalidate()));
@@ -1496,7 +1581,11 @@ export class SlackNotificationDaemon {
1496
1581
  updatedAt: this.#now(),
1497
1582
  });
1498
1583
  });
1499
- return sessionId && receipt ? { key, endpoint, sessionId, receipt } : undefined;
1584
+ if (!sessionId || !receipt) {
1585
+ await this.#journal.terminalize(effectId, { status: "rejected" });
1586
+ return undefined;
1587
+ }
1588
+ return { key, endpoint, sessionId, receipt };
1500
1589
  }
1501
1590
 
1502
1591
  async #dispatchInbound(claim: {
@@ -2020,7 +2109,8 @@ export class SlackNotificationDaemon {
2020
2109
  current.seenEventIds.includes(routing.eventId) ||
2021
2110
  current.seenInteractionIds.includes(routing.interactionId) ||
2022
2111
  current.seenRetryKeys.includes(routing.retryKey) ||
2023
- (routing.kind === "action" && replyableActionId(current.pendingActionId) !== routing.actionId)
2112
+ (routing.kind === "action" && replyableActionId(current.pendingActionId) !== routing.actionId) ||
2113
+ (routing.kind === "message" && replyableActionId(current.pendingActionId) !== undefined)
2024
2114
  )
2025
2115
  return current;
2026
2116
  receipt = {
@@ -294,6 +294,18 @@ export class SlackLiveProvider implements SlackProviderClient, SlackDiagnosticPr
294
294
  return message;
295
295
  }
296
296
 
297
+ async addReaction(input: { channel: string; timestamp: string; name: string; signal?: AbortSignal }): Promise<void> {
298
+ await this.#api(
299
+ "reactions.add",
300
+ {
301
+ channel: input.channel,
302
+ timestamp: input.timestamp,
303
+ name: input.name,
304
+ },
305
+ input.signal,
306
+ );
307
+ }
308
+
297
309
  async findMessageByClientMsgId(input: {
298
310
  channel: string;
299
311
  clientMsgId: string;
@@ -57,6 +57,7 @@ export interface SlackProviderClient {
57
57
  threadTs?: string;
58
58
  signal?: AbortSignal;
59
59
  }): Promise<SlackMessageSearchResult | null>;
60
+ addReaction?(input: { channel: string; timestamp: string; name: string; signal?: AbortSignal }): Promise<void>;
60
61
  /**
61
62
  * Prove that an operator-supplied timestamp addresses a real message in the
62
63
  * requested channel. Adoption of an existing root is refused when a client
@@ -108,6 +109,10 @@ export class SlackProvider {
108
109
  return (await this.client.findMessageByClientMsgId?.(input)) ?? null;
109
110
  }
110
111
 
112
+ async addReaction(input: { channel: string; timestamp: string; name: string; signal?: AbortSignal }): Promise<void> {
113
+ await this.client.addReaction?.(input);
114
+ }
115
+
111
116
  /** Fail-closed root verification: a client without this capability can never confirm a root. */
112
117
  async findMessageByTimestamp(input: { channel: string; ts: string }): Promise<SlackMessageSearchResult | null> {
113
118
  return (await this.client.findMessageByTimestamp?.(input)) ?? null;
@@ -78,6 +78,43 @@ export interface SdkTailEnvelopeV1 {
78
78
  items: SdkTailItemV1[];
79
79
  }
80
80
 
81
+ export function tailItemKey(item: SdkTailItemV1): string {
82
+ // (generation, seq) restarts per revision; without the revision two turns'
83
+ // first frames share "1:1" and the second is silently dropped (#5200).
84
+ if (item.generation !== undefined && item.seq !== undefined) {
85
+ if (item.revision === undefined || !Number.isSafeInteger(item.revision) || item.revision < 0)
86
+ throw new Error("Cannot key a positioned tail item without its authoritative revision.");
87
+ return `${item.kind}\u0000${item.revision}\u0000${item.generation}\u0000${item.seq}`;
88
+ }
89
+ if (item.id !== undefined) return `${item.kind}\u0000${item.id}`;
90
+ return `${item.kind}\u0000${JSON.stringify(item.payload)}`;
91
+ }
92
+
93
+ /** Holds positioned live frames until the checkpoint can stamp their authoritative revision. */
94
+ export class TailRevisionBuffer {
95
+ #pending: SdkTailItemV1[] = [];
96
+ #resolved = false;
97
+
98
+ push(item: SdkTailItemV1): SdkTailItemV1[] {
99
+ if (item.revision !== undefined || item.generation === undefined || item.seq === undefined) return [item];
100
+ if (!this.#resolved) {
101
+ this.#pending.push(item);
102
+ return [];
103
+ }
104
+ throw new Error("A positioned live tail item arrived without an authoritative revision after checkpoint.");
105
+ }
106
+
107
+ resolve(revision: number): SdkTailItemV1[] {
108
+ if (!Number.isSafeInteger(revision) || revision < 0)
109
+ throw new Error("Tail revision must be a non-negative integer.");
110
+ this.#resolved = true;
111
+ const pending = this.#pending;
112
+ this.#pending = [];
113
+ for (const item of pending) item.revision = revision;
114
+ return pending;
115
+ }
116
+ }
117
+
81
118
  const SECRET_FIELD = /(?:secret|token|password|credential|authorization|api[_-]?key)/i;
82
119
 
83
120
  /** Recursively removes secret-shaped fields (defense in depth for DTO output). */
@@ -228,12 +265,20 @@ export function toTailItemV1(
228
265
  const item: SdkTailItemV1 = {
229
266
  kind,
230
267
  ...(payloadId !== undefined ? { id: payloadId } : id !== undefined ? { id } : {}),
231
- ...(typeof value.revision === "number" ? { revision: value.revision } : {}),
268
+ ...(typeof value.revision === "number" && Number.isSafeInteger(value.revision) && value.revision >= 0
269
+ ? { revision: value.revision }
270
+ : {}),
232
271
  ...(typeof value.generation === "number" ? { generation: value.generation } : {}),
233
272
  ...(typeof value.seq === "number" ? { seq: value.seq } : {}),
234
273
  payload: stripSecretFields(payload),
235
274
  };
236
- if (fallback.revision !== undefined && item.revision === undefined) item.revision = fallback.revision;
275
+ if (
276
+ fallback.revision !== undefined &&
277
+ Number.isSafeInteger(fallback.revision) &&
278
+ fallback.revision >= 0 &&
279
+ item.revision === undefined
280
+ )
281
+ item.revision = fallback.revision;
237
282
  if (fallback.generation !== undefined && item.generation === undefined) item.generation = fallback.generation;
238
283
  if (fallback.seq !== undefined && item.seq === undefined) item.seq = fallback.seq;
239
284
  return item;
@@ -38,10 +38,13 @@ import { SESSION_REQUEST_TIMEOUT_MS } from "../session-reconnect";
38
38
  import {
39
39
  type SdkCheckpointRecordV1,
40
40
  type SdkRetentionGapV1,
41
+ type SdkRingPositionV1,
41
42
  type SdkSessionRowV1,
42
43
  type SdkTailItemV1,
43
44
  SESSION_ROWS_VERSION,
44
45
  stripSecretFields,
46
+ TailRevisionBuffer,
47
+ tailItemKey,
45
48
  toCheckpointRecordV1,
46
49
  toRetentionGapV1,
47
50
  toSessionRowV1,
@@ -331,13 +334,14 @@ function throwResponseFailure(response: unknown): void {
331
334
  async function paginatedSessionList(
332
335
  router: SessionRouter,
333
336
  input: JsonRecord = {},
334
- requestKey = `${SDK_SESSION_CLI_LIFECYCLE_ACTOR.namespace}:session.list`,
337
+ requestKey = `${SDK_SESSION_CLI_LIFECYCLE_ACTOR.namespace}:session.list:${randomBytes(12).toString("hex")}`,
335
338
  ): Promise<unknown> {
336
339
  try {
337
340
  const pages = await traverseSessionList(
338
341
  input,
339
342
  async pageInput => {
340
- const response = object(await router.listBrokerSessions(pageInput, requestKey));
343
+ const cursor = typeof pageInput.cursor === "string" ? pageInput.cursor : "initial";
344
+ const response = object(await router.listBrokerSessions(pageInput, `${requestKey}:${cursor}`));
341
345
  if (response?.ok === false) {
342
346
  const failure = object(response.error);
343
347
  throw new SdkClientError(
@@ -372,10 +376,10 @@ type SessionRows = {
372
376
  sessions: SdkSessionRowV1[];
373
377
  };
374
378
 
375
- async function sessionRows(agentDir: string): Promise<SessionRows> {
379
+ async function sessionRows(agentDir: string, input: JsonRecord = {}): Promise<SessionRows> {
376
380
  await ensureBroker({ agentDir });
377
381
  return await withRouter(agentDir, async router => {
378
- const response = await paginatedSessionList(router);
382
+ const response = await paginatedSessionList(router, input);
379
383
  const result = resultObject(response) ?? {};
380
384
  let sessions: SdkSessionRowV1[];
381
385
  try {
@@ -686,7 +690,7 @@ async function runList(agentDir: string, args: SdkSessionCliArgs): Promise<unkno
686
690
  }
687
691
 
688
692
  async function runInspect(agentDir: string, sessionId: string): Promise<unknown> {
689
- const listing = await sessionRows(agentDir);
693
+ const listing = await sessionRows(agentDir, { resolveSessionId: sessionId });
690
694
  const session = listing.sessions.find(candidate => candidate.sessionId === sessionId);
691
695
  if (!session)
692
696
  throw new SdkSessionCliError("session_unavailable", `Session ${sessionId} is not indexed by the broker.`, 1);
@@ -985,13 +989,6 @@ function extractTranscriptPage(response: unknown): { items: unknown[]; complete:
985
989
  };
986
990
  }
987
991
 
988
- function tailItemKey(item: SdkTailItemV1): string {
989
- if (item.generation !== undefined && item.seq !== undefined)
990
- return `${item.kind}\u0000${item.generation}\u0000${item.seq}`;
991
- if (item.id !== undefined) return `${item.kind}\u0000${item.id}`;
992
- return `${item.kind}\u0000${JSON.stringify(item.payload)}`;
993
- }
994
-
995
992
  function mergeTailItems(
996
993
  target: SdkTailItemV1[],
997
994
  seen: Set<string>,
@@ -1006,8 +1003,9 @@ function mergeTailItems(
1006
1003
  }
1007
1004
  }
1008
1005
 
1009
- /** Canonical ring position of an event item, when the host stated one. */
1010
- type TailSeqKey = { generation: number; seq: number };
1006
+ /** Canonical session-wide ring position of an event item, when the host stated one. */
1007
+ type TailSeqKey = SdkRingPositionV1;
1008
+ type TailLocalPosition = Pick<SdkRingPositionV1, "generation" | "seq">;
1011
1009
 
1012
1010
  function isPositionCoordinate(value: unknown): value is number {
1013
1011
  return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
@@ -1057,7 +1055,7 @@ function classifyRawEventPosition(event: unknown): "unpositioned" | "malformed"
1057
1055
  * it is never silently downgraded to unpositioned. Claiming neither coordinate
1058
1056
  * is a genuine unpositioned event.
1059
1057
  */
1060
- function classifyTailPosition(item: SdkTailItemV1): TailSeqKey | "unpositioned" | "malformed" {
1058
+ function classifyTailPosition(item: SdkTailItemV1): TailLocalPosition | "unpositioned" | "malformed" {
1061
1059
  if (item.generation === undefined && item.seq === undefined) return "unpositioned";
1062
1060
  if (!isPositionCoordinate(item.generation) || !isPositionCoordinate(item.seq)) return "malformed";
1063
1061
  return { generation: item.generation, seq: item.seq };
@@ -1065,10 +1063,13 @@ function classifyTailPosition(item: SdkTailItemV1): TailSeqKey | "unpositioned"
1065
1063
 
1066
1064
  function tailSeqKey(item: SdkTailItemV1): TailSeqKey | undefined {
1067
1065
  const position = classifyTailPosition(item);
1068
- return typeof position === "string" ? undefined : position;
1066
+ return typeof position === "string" || !isPositionCoordinate(item.revision)
1067
+ ? undefined
1068
+ : { revision: item.revision, ...position };
1069
1069
  }
1070
1070
 
1071
1071
  function compareTailSeqKeys(left: TailSeqKey, right: TailSeqKey): number {
1072
+ if (left.revision !== right.revision) return left.revision - right.revision;
1072
1073
  return left.generation !== right.generation ? left.generation - right.generation : left.seq - right.seq;
1073
1074
  }
1074
1075
 
@@ -1112,10 +1113,12 @@ function mergeEventTailItems(
1112
1113
  target.unpositioned.push(item);
1113
1114
  continue;
1114
1115
  }
1116
+ const orderedPosition = tailSeqKey(item);
1117
+ if (orderedPosition === undefined) throw new Error("A positioned tail item has no authoritative revision.");
1115
1118
  let index = target.positioned.length;
1116
1119
  for (; index > 0; index--) {
1117
1120
  const previous = tailSeqKey(target.positioned[index - 1]!);
1118
- if (previous === undefined || compareTailSeqKeys(previous, position) <= 0) break;
1121
+ if (previous === undefined || compareTailSeqKeys(previous, orderedPosition) <= 0) break;
1119
1122
  }
1120
1123
  target.positioned.splice(index, 0, item);
1121
1124
  }
@@ -1158,11 +1161,13 @@ function tailItemFromRouterFrame(frame: SessionRouterFrame): SdkTailItemV1 | und
1158
1161
  return toTailItemV1(
1159
1162
  {
1160
1163
  kind: frame.name,
1164
+ ...(frame.revision === undefined ? {} : { revision: frame.revision }),
1161
1165
  ...(seq === undefined ? {} : { generation: frame.generation, seq }),
1162
1166
  payload: frame.body,
1163
1167
  },
1164
1168
  {
1165
1169
  kind: frame.name,
1170
+ ...(frame.revision === undefined ? {} : { revision: frame.revision }),
1166
1171
  ...(seq === undefined ? {} : { generation: frame.generation, seq }),
1167
1172
  },
1168
1173
  );
@@ -1406,6 +1411,7 @@ async function runLiveTail(
1406
1411
  // evidence.
1407
1412
  const seenTranscript = new Set<string>();
1408
1413
  const seenEvents = new Set<string>();
1414
+ const liveRevisionBuffer = new TailRevisionBuffer();
1409
1415
  let checkpoint: SdkCheckpointRecordV1 | undefined;
1410
1416
  let gap: SdkRetentionGapV1 | undefined;
1411
1417
  let liveReason: TailExitReason | undefined;
@@ -1445,7 +1451,7 @@ async function runLiveTail(
1445
1451
  // close at a contested position win instead of failing closed, and
1446
1452
  // checking it after the monotonic guard would silently skip a
1447
1453
  // conflicting kind at an already-decided position.
1448
- const positionKey = `${position.generation}\u0000${position.seq}`;
1454
+ const positionKey = `${position.revision}\u0000${position.generation}\u0000${position.seq}`;
1449
1455
  const claimed = claimedLifecycleKinds.get(positionKey);
1450
1456
  if (claimed !== undefined && claimed !== item.kind) {
1451
1457
  malformed ??= new SdkSessionCliError(
@@ -1487,9 +1493,17 @@ async function runLiveTail(
1487
1493
  if (attachment.sessionId !== sessionId) return;
1488
1494
  const item = tailItemFromRouterFrame(frame);
1489
1495
  if (!item) return;
1490
- if (item.revision === undefined && checkpoint?.revision !== undefined) item.revision = checkpoint.revision;
1491
- // If checkpoint was undefined, queue for later stamping? For now leave without rev
1492
- applyLifecycle(mergeEventTailItems(eventItems, seenEvents, [item], include));
1496
+ try {
1497
+ applyLifecycle(mergeEventTailItems(eventItems, seenEvents, liveRevisionBuffer.push(item), include));
1498
+ } catch (error) {
1499
+ malformed ??= new SdkSessionCliError(
1500
+ "protocol_error",
1501
+ error instanceof Error ? error.message : "A live tail item had no authoritative revision.",
1502
+ 1,
1503
+ { sessionId, kind: item.kind, generation: item.generation, seq: item.seq },
1504
+ );
1505
+ rejectLive?.(malformed);
1506
+ }
1493
1507
  };
1494
1508
 
1495
1509
  return await withRouter(
@@ -1511,6 +1525,10 @@ async function runLiveTail(
1511
1525
  const extraction = extractCheckpoint(checkpointResponse);
1512
1526
  checkpoint = extraction.record;
1513
1527
  gap = extraction.gap;
1528
+ if (checkpoint !== undefined)
1529
+ applyLifecycle(
1530
+ mergeEventTailItems(eventItems, seenEvents, liveRevisionBuffer.resolve(checkpoint.revision), include),
1531
+ );
1514
1532
  if (gap !== undefined && args.strict === true)
1515
1533
  throw new SdkSessionCliError(
1516
1534
  "retention_gap",
@@ -1595,7 +1613,11 @@ async function runLiveTail(
1595
1613
  applyLifecycle(mergeEventTailItems(eventItems, seenEvents, replayItems, include));
1596
1614
  if (malformed !== undefined) throw malformed;
1597
1615
  if (checkpoint !== undefined) {
1598
- const checkpointKey = { generation: checkpoint.generation, seq: checkpoint.seq };
1616
+ const checkpointKey = {
1617
+ revision: checkpoint.revision,
1618
+ generation: checkpoint.generation,
1619
+ seq: checkpoint.seq,
1620
+ };
1599
1621
  if (turnStateKey === undefined || compareTailSeqKeys(checkpointKey, turnStateKey) >= 0) {
1600
1622
  turnStateKey = checkpointKey;
1601
1623
  turnIdle = checkpoint.idle;
@@ -1650,7 +1672,9 @@ export async function runTail(
1650
1672
  sessionId: string,
1651
1673
  args: SdkSessionCliArgs,
1652
1674
  ): Promise<unknown> {
1653
- const row = (await sessionRows(agentDir)).sessions.find(candidate => candidate.sessionId === sessionId);
1675
+ const row = (await sessionRows(agentDir, { resolveSessionId: sessionId })).sessions.find(
1676
+ candidate => candidate.sessionId === sessionId,
1677
+ );
1654
1678
  if (!row)
1655
1679
  throw new SdkSessionCliError("session_unavailable", `Session ${sessionId} is not indexed by the broker.`, 1);
1656
1680
  if (row.deleted)
@@ -32,11 +32,20 @@ export class SessionEventStream {
32
32
  #frames: EventFrame[] = [];
33
33
  readonly #ringSize: number;
34
34
  readonly #resyncQueries: string[];
35
+ readonly #revisionProvider: (() => number | undefined) | undefined;
35
36
 
36
- constructor(options: { generation?: number; ringSize?: number; resyncQueryIds?: string[] } = {}) {
37
+ constructor(
38
+ options: {
39
+ generation?: number;
40
+ ringSize?: number;
41
+ resyncQueryIds?: string[];
42
+ revisionProvider?: () => number | undefined;
43
+ } = {},
44
+ ) {
37
45
  this.#generation = options.generation ?? 0;
38
46
  this.#ringSize = options.ringSize ?? 256;
39
47
  this.#resyncQueries = options.resyncQueryIds ?? ["Q01", "Q02", "Q03"];
48
+ this.#revisionProvider = options.revisionProvider;
40
49
  }
41
50
 
42
51
  get generation(): number {
@@ -54,7 +63,21 @@ export class SessionEventStream {
54
63
  }
55
64
 
56
65
  emit(frame: SdkFrame): EventFrame {
57
- const event: EventFrame = { ...frame, type: "event", generation: this.#generation, seq: ++this.#seq };
66
+ const suppliedRevision = frame.revision;
67
+ const providedRevision = this.#revisionProvider?.();
68
+ const revision =
69
+ typeof suppliedRevision === "number" && Number.isSafeInteger(suppliedRevision) && suppliedRevision >= 0
70
+ ? suppliedRevision
71
+ : typeof providedRevision === "number" && Number.isSafeInteger(providedRevision) && providedRevision >= 0
72
+ ? providedRevision
73
+ : undefined;
74
+ const event: EventFrame = {
75
+ ...frame,
76
+ type: "event",
77
+ ...(revision === undefined ? {} : { revision }),
78
+ generation: this.#generation,
79
+ seq: ++this.#seq,
80
+ };
58
81
  this.#frames.push(event);
59
82
  if (this.#frames.length > this.#ringSize) this.#frames.shift();
60
83
  return event;
@@ -47,6 +47,8 @@ export type SessionActivationGate = (input: { sessionId: string; generation: num
47
47
  export interface SessionSdkHostOptions extends HostEndpointAdapters {
48
48
  control?: (connectionId: string, frame: SdkFrame) => unknown | Promise<unknown>;
49
49
  query?: (connectionId: string, frame: SdkFrame) => unknown | Promise<unknown>;
50
+ /** Supplies the durable transcript revision stamped on ordinary emitted events. */
51
+ eventRevision?: () => number | undefined;
50
52
  /** Test/lifecycle seam invoked synchronously before fire-and-forget dispatch. */
51
53
  onFrameAdmitted?: (connectionId: string, frame: SdkFrame) => void;
52
54
  /** Synchronously rejects external input while a runtime is being replaced. */
@@ -199,7 +201,7 @@ function has(frame: SdkFrame, field: string): boolean {
199
201
 
200
202
  /** Adapter-based session host; bus wiring owns NotificationServer creation and transport framing. */
201
203
  export class SessionSdkHost {
202
- readonly events = new SessionEventStream();
204
+ readonly events: SessionEventStream;
203
205
  readonly reverse: ReverseLeaseRuntime;
204
206
  readonly #options: SessionSdkHostOptions;
205
207
  #started = false;
@@ -214,6 +216,7 @@ export class SessionSdkHost {
214
216
 
215
217
  constructor(options: SessionSdkHostOptions) {
216
218
  this.#options = options;
219
+ this.events = new SessionEventStream({ revisionProvider: options.eventRevision });
217
220
  this.reverse = new ReverseLeaseRuntime({
218
221
  sendFrame: options.sendFrame,
219
222
  installDefinitions: options.installProviderDefinitions,