@gajae-code/coding-agent 0.16.0 → 0.16.3

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 (164) hide show
  1. package/CHANGELOG.md +52 -1
  2. package/dist/types/async/job-manager.d.ts +32 -1
  3. package/dist/types/browser-backend.d.ts +12 -0
  4. package/dist/types/cli/update-cli.d.ts +1 -0
  5. package/dist/types/commands/sdk.d.ts +2 -0
  6. package/dist/types/config/billing-path.d.ts +25 -0
  7. package/dist/types/config/config-schema-version.d.ts +13 -1
  8. package/dist/types/config/model-registry.d.ts +16 -0
  9. package/dist/types/config/settings-schema.d.ts +17 -7
  10. package/dist/types/extensibility/extensions/types.d.ts +8 -3
  11. package/dist/types/gjc-runtime/launch-worktree.d.ts +25 -1
  12. package/dist/types/gjc-runtime/session-state-lock.d.ts +22 -0
  13. package/dist/types/main.d.ts +1 -0
  14. package/dist/types/modes/acp/acp-agent.d.ts +21 -21
  15. package/dist/types/modes/components/model-selector.d.ts +4 -0
  16. package/dist/types/modes/components/session-selector.d.ts +1 -1
  17. package/dist/types/modes/components/sessions-dashboard.d.ts +2 -1
  18. package/dist/types/modes/interactive-mode.d.ts +16 -2
  19. package/dist/types/modes/theme/theme.d.ts +2 -1
  20. package/dist/types/modes/types.d.ts +2 -0
  21. package/dist/types/sdk/broker/broker.d.ts +2 -1
  22. package/dist/types/sdk/broker/ensure.d.ts +3 -0
  23. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +6 -0
  24. package/dist/types/sdk/broker/lifecycle.d.ts +14 -0
  25. package/dist/types/sdk/broker/startup-budget.d.ts +52 -0
  26. package/dist/types/sdk/broker/state-version.d.ts +17 -0
  27. package/dist/types/sdk/bus/discord-daemon.d.ts +1 -1
  28. package/dist/types/sdk/bus/index.d.ts +3 -1
  29. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +1 -1
  30. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +1 -1
  31. package/dist/types/sdk/bus/slack-conversation.d.ts +1 -1
  32. package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
  33. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
  34. package/dist/types/sdk/cli/rows.d.ts +15 -2
  35. package/dist/types/sdk/cli/session-cli.d.ts +2 -1
  36. package/dist/types/sdk/host/events.d.ts +2 -0
  37. package/dist/types/sdk/host/host.d.ts +2 -0
  38. package/dist/types/sdk/host/query/cursor.d.ts +4 -2
  39. package/dist/types/sdk/host/query/handlers.d.ts +1 -0
  40. package/dist/types/sdk/host/query/revision-store.d.ts +1 -0
  41. package/dist/types/sdk/host/session-runtime.d.ts +18 -17
  42. package/dist/types/sdk/lifecycle-session.d.ts +1 -1
  43. package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -5
  44. package/dist/types/sdk/receipt-state.d.ts +2 -0
  45. package/dist/types/sdk/router/session-router.d.ts +4 -0
  46. package/dist/types/sdk/turn-result.d.ts +6 -0
  47. package/dist/types/session/agent-session.d.ts +47 -7
  48. package/dist/types/session/fold-coordinator.d.ts +29 -2
  49. package/dist/types/session/session-manager.d.ts +14 -2
  50. package/dist/types/tools/ask.d.ts +1 -0
  51. package/dist/types/tools/bash.d.ts +12 -0
  52. package/dist/types/tools/browser/launch.d.ts +10 -0
  53. package/dist/types/tools/browser/registry.d.ts +8 -0
  54. package/dist/types/tools/index.d.ts +4 -10
  55. package/dist/types/tools/job.d.ts +3 -1
  56. package/dist/types/utils/external-editor.d.ts +12 -0
  57. package/examples/extensions/README.md +1 -1
  58. package/examples/extensions/pirate.ts +7 -4
  59. package/package.json +7 -7
  60. package/scripts/generate-sdk-operation-inventory.ts +7 -1
  61. package/src/async/job-manager.ts +107 -9
  62. package/src/browser-backend.ts +29 -0
  63. package/src/cli/plugin-cli.ts +29 -0
  64. package/src/cli/update-cli.ts +63 -12
  65. package/src/commands/sdk.ts +22 -7
  66. package/src/config/billing-path.ts +42 -0
  67. package/src/config/config-schema-version.ts +18 -1
  68. package/src/config/model-preset-registry.ts +177 -18
  69. package/src/config/model-registry.ts +268 -42
  70. package/src/config/settings-schema.ts +19 -7
  71. package/src/config/settings.ts +46 -15
  72. package/src/coordinator-mcp/projection-scan.ts +6 -6
  73. package/src/coordinator-mcp/server.ts +21 -1
  74. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
  75. package/src/extensibility/extensions/runner.ts +27 -0
  76. package/src/extensibility/extensions/types.ts +8 -3
  77. package/src/extensibility/gjc-plugins/installer.ts +51 -11
  78. package/src/extensibility/runtime-skill-discovery.ts +110 -3
  79. package/src/extensibility/skills.ts +22 -12
  80. package/src/gjc-runtime/launch-worktree.ts +285 -4
  81. package/src/gjc-runtime/session-state-lock.ts +156 -32
  82. package/src/gjc-runtime/session-state-sidecar.ts +10 -17
  83. package/src/gjc-runtime/tmux-owner-isolation.ts +16 -1
  84. package/src/internal-urls/docs-index.generated.ts +11 -11
  85. package/src/main.ts +4 -1
  86. package/src/modes/acp/acp-agent.ts +34 -19
  87. package/src/modes/components/model-selector.ts +76 -8
  88. package/src/modes/components/session-selector.ts +23 -15
  89. package/src/modes/components/sessions-dashboard.ts +8 -5
  90. package/src/modes/controllers/command-controller.ts +1 -1
  91. package/src/modes/controllers/event-controller.ts +30 -4
  92. package/src/modes/controllers/extension-ui-controller.ts +13 -13
  93. package/src/modes/controllers/input-controller.ts +2 -2
  94. package/src/modes/controllers/selector-controller.ts +10 -3
  95. package/src/modes/interactive-mode.ts +36 -12
  96. package/src/modes/prompt-action-autocomplete.ts +30 -4
  97. package/src/modes/runtime-init.ts +12 -9
  98. package/src/modes/theme/theme.ts +5 -0
  99. package/src/modes/types.ts +2 -0
  100. package/src/modes/utils/ui-helpers.ts +22 -3
  101. package/src/prompts/agent-fragments/browser-aside.md +22 -0
  102. package/src/prompts/tools/ask.md +1 -1
  103. package/src/prompts/tools/bash.md +1 -0
  104. package/src/prompts/tools/job.md +1 -1
  105. package/src/sdk/acp/adapter.ts +6 -1
  106. package/src/sdk/broker/broker.ts +10 -1
  107. package/src/sdk/broker/ensure.ts +76 -3
  108. package/src/sdk/broker/lifecycle-ledger.ts +6 -0
  109. package/src/sdk/broker/lifecycle.ts +254 -34
  110. package/src/sdk/broker/spawn-substrate.ts +45 -2
  111. package/src/sdk/broker/startup-budget.ts +144 -2
  112. package/src/sdk/broker/state-version.ts +17 -0
  113. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  114. package/src/sdk/bus/chat-daemon-runtime.ts +118 -24
  115. package/src/sdk/bus/discord-daemon.ts +50 -10
  116. package/src/sdk/bus/index.ts +33 -3
  117. package/src/sdk/bus/kind-aware-reconciliation.ts +296 -48
  118. package/src/sdk/bus/lifecycle-commands.ts +4 -0
  119. package/src/sdk/bus/notification-service.ts +22 -5
  120. package/src/sdk/bus/prompt-reconciliation.ts +7 -2
  121. package/src/sdk/bus/reconciliation-store.ts +55 -31
  122. package/src/sdk/bus/slack-conversation.ts +1 -1
  123. package/src/sdk/bus/slack-daemon.ts +72 -38
  124. package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
  125. package/src/sdk/cli/rows.ts +75 -7
  126. package/src/sdk/cli/session-cli.ts +68 -35
  127. package/src/sdk/host/control/dispatch.ts +2 -2
  128. package/src/sdk/host/events.ts +26 -2
  129. package/src/sdk/host/host.ts +6 -1
  130. package/src/sdk/host/query/cursor.ts +52 -7
  131. package/src/sdk/host/query/handlers.ts +33 -12
  132. package/src/sdk/host/query/revision-store.ts +12 -0
  133. package/src/sdk/host/session-runtime.test.ts +497 -31
  134. package/src/sdk/host/session-runtime.ts +387 -54
  135. package/src/sdk/host/surface-policy.ts +3 -3
  136. package/src/sdk/lifecycle-session.ts +7 -2
  137. package/src/sdk/prompt-deadline-manager.ts +2 -1
  138. package/src/sdk/protocol/operation-inventory.generated.json +54 -3
  139. package/src/sdk/protocol/operation-registry.ts +1 -1
  140. package/src/sdk/receipt-state.ts +10 -0
  141. package/src/sdk/router/session-router.ts +210 -94
  142. package/src/sdk/session.ts +59 -41
  143. package/src/sdk/turn-result.ts +22 -0
  144. package/src/session/agent-session.ts +1033 -260
  145. package/src/session/fold-coordinator.ts +40 -3
  146. package/src/session/session-manager.ts +517 -77
  147. package/src/slash-commands/builtin-registry.ts +41 -0
  148. package/src/task/executor.ts +9 -4
  149. package/src/tools/ask-contract.test.ts +64 -0
  150. package/src/tools/ask-contract.ts +36 -1
  151. package/src/tools/ask.ts +10 -8
  152. package/src/tools/bash-interactive.ts +11 -2
  153. package/src/tools/bash.ts +158 -33
  154. package/src/tools/browser/launch.ts +37 -0
  155. package/src/tools/browser/registry.ts +99 -9
  156. package/src/tools/cron.ts +6 -4
  157. package/src/tools/descriptors.test.ts +15 -0
  158. package/src/tools/descriptors.ts +5 -1
  159. package/src/tools/index.ts +4 -6
  160. package/src/tools/job.ts +4 -1
  161. package/src/tools/monitor.ts +12 -9
  162. package/src/tools/resolve.ts +30 -10
  163. package/src/tools/tool-catalog.generated.ts +2 -2
  164. package/src/utils/external-editor.ts +86 -1
@@ -14,7 +14,7 @@ import * as fs from "node:fs/promises";
14
14
  import * as path from "node:path";
15
15
  import type { PromptReconciliationStatus, SdkPromptTerminalOutcome } from "../prompt-status";
16
16
  import type { ReceiptState } from "../receipt-state";
17
- import { TURN_RESULT_CONTENT_MAX_BYTES, type TurnResultContent } from "../turn-result";
17
+ import { reportableTurnResultContent, TURN_RESULT_CONTENT_MAX_BYTES, type TurnResultContent } from "../turn-result";
18
18
  import type { PromptCorrelation } from "./prompt-reconciliation";
19
19
 
20
20
  export const RECONCILIATION_STORE_VERSION = 2;
@@ -25,6 +25,16 @@ export const RECONCILIATION_DIR_NAME = ".sdk-reconciliation";
25
25
  // durable document can never match a real retry and would make the reserved key
26
26
  // invisible (and reusable for an unrelated later abort), so reload rejects it.
27
27
  const SHA256_RE = /^[0-9a-f]{64}$/;
28
+ const SAFE_FAILURE_CODE_RE = /^[A-Za-z0-9._-]{1,64}$/;
29
+ const DURABLE_FAILURE_MESSAGE_MAX_BYTES = 4_096;
30
+
31
+ function isSafeFailureDiagnostic(value: unknown): value is { code: string; message: string } {
32
+ if (!isRecord(value) || typeof value.code !== "string" || !SAFE_FAILURE_CODE_RE.test(value.code)) return false;
33
+ return (
34
+ typeof value.message === "string" &&
35
+ new TextEncoder().encode(value.message).length <= DURABLE_FAILURE_MESSAGE_MAX_BYTES
36
+ );
37
+ }
28
38
 
29
39
  export function resolveReconciliationSessionFile(
30
40
  sessionFile: string | undefined | null,
@@ -207,11 +217,7 @@ function isValidRecord(value: unknown): boolean {
207
217
  const settled = value.status !== "dispatching";
208
218
  if (settled !== (typeof value.settledAt === "number" && Number.isFinite(value.settledAt))) return false;
209
219
  if ((value.status === "rejected" || value.status === "uncertain") !== (value.error !== undefined)) return false;
210
- if (
211
- value.error !== undefined &&
212
- (!isRecord(value.error) || typeof value.error.code !== "string" || typeof value.error.message !== "string")
213
- )
214
- return false;
220
+ if (value.error !== undefined && !isSafeFailureDiagnostic(value.error)) return false;
215
221
  return value.receiptState === undefined && value.outcome === undefined;
216
222
  }
217
223
  if (kind !== "prompt" && kind !== "skill") return false;
@@ -274,8 +280,7 @@ function isValidRecord(value: unknown): boolean {
274
280
  return false;
275
281
  }
276
282
  if (value.error !== undefined) {
277
- if (!isRecord(value.error)) return false;
278
- if (typeof value.error.code !== "string" || typeof value.error.message !== "string") return false;
283
+ if (!isSafeFailureDiagnostic(value.error)) return false;
279
284
  }
280
285
  return [outcome, pendingOutcome].every(candidate => {
281
286
  if (candidate === undefined) return true;
@@ -286,12 +291,13 @@ function isValidRecord(value: unknown): boolean {
286
291
  candidate.reason as string,
287
292
  ) && ["agent", "client_cancel"].includes(candidate.provenance as string)
288
293
  );
289
- return (
290
- candidate.kind === "failed" &&
291
- ["prompt_failed", "prompt_deadline_exceeded"].includes(candidate.code as string) &&
292
- typeof candidate.message === "string" &&
293
- ["agent_failed", "deadline"].includes(candidate.provenance as string)
294
- );
294
+ if (
295
+ candidate.kind !== "failed" ||
296
+ !["prompt_failed", "prompt_deadline_exceeded"].includes(candidate.code as string) ||
297
+ !["agent_failed", "deadline"].includes(candidate.provenance as string)
298
+ )
299
+ return false;
300
+ return isSafeFailureDiagnostic({ code: candidate.code, message: candidate.message });
295
301
  });
296
302
  }
297
303
  /** Terminal scope validation: bounded origin/fence/settlement fields only. */
@@ -361,8 +367,9 @@ function isValidTerminalScope(value: unknown): boolean {
361
367
  responseState !== "failed"
362
368
  )
363
369
  return false;
364
- if (typeof responsePayloadHash !== "string" || !responsePayloadHash) return false;
365
- if (replayPayloadHash !== undefined && (typeof replayPayloadHash !== "string" || !replayPayloadHash)) return false;
370
+ if (typeof responsePayloadHash !== "string" || !SHA256_RE.test(responsePayloadHash)) return false;
371
+ if (replayPayloadHash !== undefined && (typeof replayPayloadHash !== "string" || !SHA256_RE.test(replayPayloadHash)))
372
+ return false;
366
373
  if (typeof idempotencyKeyHash !== "string" || !SHA256_RE.test(idempotencyKeyHash)) return false;
367
374
  if (typeof idempotencyInputHash !== "string" || !SHA256_RE.test(idempotencyInputHash)) return false;
368
375
  if (terminalPublished !== undefined && typeof terminalPublished !== "boolean") return false;
@@ -406,12 +413,12 @@ function isValidEvictedTerminalKeyEntry(value: unknown): value is EvictedTermina
406
413
  return false;
407
414
  if (
408
415
  value.responsePayloadHash !== undefined &&
409
- (typeof value.responsePayloadHash !== "string" || !value.responsePayloadHash)
416
+ (typeof value.responsePayloadHash !== "string" || !SHA256_RE.test(value.responsePayloadHash))
410
417
  )
411
418
  return false;
412
419
  if (
413
420
  value.replayPayloadHash !== undefined &&
414
- (typeof value.replayPayloadHash !== "string" || !value.replayPayloadHash)
421
+ (typeof value.replayPayloadHash !== "string" || !SHA256_RE.test(value.replayPayloadHash))
415
422
  )
416
423
  return false;
417
424
  if (value.terminalPublished !== undefined && typeof value.terminalPublished !== "boolean") return false;
@@ -550,6 +557,14 @@ export function settleProcessRestart(
550
557
  records: DurableReconciliationRecord[],
551
558
  now: number,
552
559
  ): DurableReconciliationRecord[] {
560
+ const isDeadlineOutcome = (outcome: SdkPromptTerminalOutcome | undefined): boolean =>
561
+ outcome?.kind === "failed" && outcome.provenance === "deadline";
562
+ const providerFailureOutcome = (error: { code: string; message: string }): SdkPromptTerminalOutcome => ({
563
+ kind: "failed",
564
+ code: "prompt_failed",
565
+ message: error.message,
566
+ provenance: "agent_failed",
567
+ });
553
568
  return records.map(record => {
554
569
  if (record.kind === "steer") {
555
570
  if (record.status !== "dispatching") return record;
@@ -569,29 +584,38 @@ export function settleProcessRestart(
569
584
  return record;
570
585
  }
571
586
  if (record.pendingOutcome !== undefined || record.kind === "prompt") {
587
+ const { pendingOutcome, pendingReceiptState, ...settledRecord } = record;
588
+ const providerErrorRecord = record.error;
589
+ const providerError =
590
+ providerErrorRecord !== undefined && providerErrorRecord.code !== "prompt_deadline_exceeded";
591
+ const preserveProviderError = providerError;
572
592
  const outcome: SdkPromptTerminalOutcome =
573
- record.error !== undefined && record.pendingOutcome?.kind !== "failed"
574
- ? {
575
- kind: "failed",
576
- code: "prompt_failed",
577
- message: record.error.message,
578
- provenance: "agent_failed",
579
- }
580
- : (record.pendingOutcome ?? {
593
+ providerError &&
594
+ (pendingOutcome === undefined || isDeadlineOutcome(pendingOutcome) || pendingOutcome.kind === "stopped")
595
+ ? providerFailureOutcome(providerErrorRecord)
596
+ : (pendingOutcome ?? {
581
597
  kind: "failed",
582
598
  code: "prompt_failed",
583
599
  message: "Prompt did not complete before process restart.",
584
600
  provenance: "agent_failed",
585
601
  });
602
+ const preservedError =
603
+ providerError && preserveProviderError
604
+ ? providerErrorRecord
605
+ : outcome.kind === "failed"
606
+ ? { code: outcome.code, message: outcome.message }
607
+ : undefined;
608
+ const receiptState = reportableTurnResultContent(record.content)
609
+ ? "present"
610
+ : (pendingReceiptState ?? (outcome.kind === "stopped" ? "missing" : "unknown"));
586
611
  return {
587
- ...record,
612
+ ...settledRecord,
588
613
  status: outcome.kind === "stopped" ? "terminal_ok" : "failed",
589
614
  terminalAt: now,
590
615
  outcome,
591
- receiptState: record.pendingReceiptState ?? (outcome.kind === "stopped" ? "missing" : "unknown"),
592
- pendingOutcome: undefined,
593
- pendingReceiptState: undefined,
594
- ...(outcome.kind === "failed" ? { error: { code: outcome.code, message: outcome.message } } : {}),
616
+ receiptState,
617
+
618
+ ...(preservedError !== undefined ? { error: preservedError } : { error: undefined }),
595
619
  };
596
620
  }
597
621
  return {
@@ -6,7 +6,7 @@ export interface SlackInboundDispatchReceipt {
6
6
  interactionId: string;
7
7
  retryKey: string;
8
8
  eventContext?: string;
9
- kind: "action" | "command";
9
+ kind: "action" | "command" | "message";
10
10
  actionId?: string;
11
11
  /** The endpoint generation and SDK effect captured before Socket Mode ACK. */
12
12
  endpointGeneration: number;
@@ -145,13 +145,14 @@ type SlackInboundRouting = {
145
145
  interactionId: string;
146
146
  retryKey: string;
147
147
  eventContext?: string;
148
- kind: "action" | "command";
148
+ kind: "action" | "command" | "message";
149
149
  actionId?: string;
150
150
  };
151
151
 
152
152
  type SlackInboundEffectPayload =
153
153
  | { type: "reply"; id: string; answer: string; idempotencyKey: string; routing: SlackInboundRouting }
154
- | { type: "command"; content: string; idempotencyKey: string; routing: SlackInboundRouting };
154
+ | { type: "command"; content: string; idempotencyKey: string; routing: SlackInboundRouting }
155
+ | { type: "user_message"; sessionId: string; text: string; idempotencyKey: string; routing: SlackInboundRouting };
155
156
 
156
157
  type SlackRootPublication = {
157
158
  conversation: SlackConversation;
@@ -169,7 +170,11 @@ function messageFromEnvelope(
169
170
  | undefined {
170
171
  if (!payload || typeof payload !== "object") return undefined;
171
172
  const eventPayload = payload as EventsPayload;
172
- if (eventPayload.type !== "events_api" || !eventPayload.event || eventPayload.event.type !== "message")
173
+ if (
174
+ (eventPayload.type !== "events_api" && eventPayload.type !== "event_callback") ||
175
+ !eventPayload.event ||
176
+ eventPayload.event.type !== "message"
177
+ )
173
178
  return undefined;
174
179
  const eventId = text(eventPayload.event_id);
175
180
  const teamId = text(eventPayload.team_id);
@@ -191,6 +196,10 @@ function nextRecord(record: SlackConversation, update: Partial<SlackConversation
191
196
  return normalizeSlackConversation({ ...record, ...update, generation: record.generation + 1 });
192
197
  }
193
198
 
199
+ function replyableActionId(actionId: string | undefined): string | undefined {
200
+ return actionId?.startsWith("idle:") ? undefined : actionId;
201
+ }
202
+
194
203
  /**
195
204
  * Slack Socket Mode notification daemon. Accepted inbound SDK effects are
196
205
  * persisted before acknowledgement, including their replay payload and captured
@@ -449,7 +458,7 @@ export class SlackNotificationDaemon {
449
458
  const actorId = text(inbound.event.user);
450
459
  if (
451
460
  inbound.event.bot_id ||
452
- inbound.event.subtype === "bot_message" ||
461
+ inbound.event.subtype !== undefined ||
453
462
  actorId === this.options.botUserId ||
454
463
  !actorId ||
455
464
  !(await this.#actorAuthorized(actorId))
@@ -852,7 +861,7 @@ export class SlackNotificationDaemon {
852
861
  publicationId === undefined
853
862
  ? undefined
854
863
  : await this.#publicationAttempt(
855
- `root:${sessionId}:${publicationId}`,
864
+ `root:${this.options.teamId}:${this.options.channelId}:${sessionId}:${publicationId}`,
856
865
  clientMsgId => `root:${sessionId}:${clientMsgId}`,
857
866
  );
858
867
  const existing = await this.findSession(sessionId, false);
@@ -1039,16 +1048,21 @@ export class SlackNotificationDaemon {
1039
1048
  return await this.#track(this.#close(sessionId, marker, true, endpointGeneration));
1040
1049
  }
1041
1050
 
1042
- async recoverCleanup(sessionId: string, endpointGeneration: number): Promise<void> {
1051
+ async recoverCleanup(sessionId: string, endpointGeneration: number, authorityId?: string): Promise<boolean> {
1043
1052
  const found = await this.findSession(sessionId, true);
1053
+ if (!found) return true;
1054
+ if (found.record.endpointGeneration !== endpointGeneration) return true;
1055
+ if (found.record.state === "closed_marker" || found.record.state === "error") return true;
1044
1056
  if (
1045
- !found?.record.rootTs ||
1046
- found.record.state !== "active" ||
1047
- found.record.endpointGeneration !== endpointGeneration
1057
+ (found.record.state === "posting_root" || found.record.state === "resumed_root") &&
1058
+ found.record.attachmentAuthorityId === authorityId
1048
1059
  )
1049
- return;
1060
+ return true;
1061
+ if (found.record.state !== "active") return false;
1062
+ if (!found.record.rootTs) return false;
1063
+ if (found.record.attachmentAuthorityId === authorityId && found.record.cleanupEffectId === undefined) return true;
1050
1064
  const effectId = `close-marker-cleanup:${sessionId}:${found.record.clientMsgId ?? found.record.rootTs}`;
1051
- if (found.record.cleanupEffectId !== effectId) return;
1065
+ if (found.record.cleanupEffectId !== effectId) return false;
1052
1066
  const effect = await this.#journal.read(effectId);
1053
1067
  if (!effect || (effect.state === "terminal" && !effect.receipt?.messageId && !effect.receipt?.timestamp)) {
1054
1068
  await this.store.transact(found.key, current =>
@@ -1056,9 +1070,10 @@ export class SlackNotificationDaemon {
1056
1070
  ? nextRecord(current, { cleanupEffectId: undefined, updatedAt: this.#now() })
1057
1071
  : current,
1058
1072
  );
1059
- return;
1073
+ return false;
1060
1074
  }
1061
1075
  await this.close(sessionId, "Session closed.", endpointGeneration);
1076
+ return true;
1062
1077
  }
1063
1078
 
1064
1079
  async retireAttachment(sessionId: string, endpointGeneration: number): Promise<void> {
@@ -1209,7 +1224,7 @@ export class SlackNotificationDaemon {
1209
1224
  publicationId === undefined
1210
1225
  ? undefined
1211
1226
  : await this.#publicationAttempt(
1212
- `root:${sessionId}:${publicationId}`,
1227
+ `root:${this.options.teamId}:${this.options.channelId}:${sessionId}:${publicationId}`,
1213
1228
  clientMsgId => `root:${sessionId}:${clientMsgId}`,
1214
1229
  );
1215
1230
  return (await this.#resumeWithRootPublication(sessionId, body, endpointGeneration, rootPublication?.clientMsgId))
@@ -1389,7 +1404,10 @@ export class SlackNotificationDaemon {
1389
1404
  const interactionId = text(inbound.event.client_msg_id) ?? inbound.eventId;
1390
1405
  const retryKey = `${inbound.eventId}:${interactionId}`;
1391
1406
  const inboundText = text(inbound.event.text);
1392
- const command = inboundText?.startsWith("/sdk ") ?? false;
1407
+ if (!inboundText) return undefined;
1408
+ const command = inboundText.startsWith("/sdk ");
1409
+ const actionId = command ? undefined : replyableActionId(record.pendingActionId);
1410
+ const kind: SlackInboundRouting["kind"] = command ? "command" : actionId ? "action" : "message";
1393
1411
 
1394
1412
  const idempotencyKey = `slack:${inbound.teamId}:${inbound.channelId}:${inbound.rootTs}:${actorId}:${inbound.eventId}:${interactionId}`;
1395
1413
  const effectId = `inbound:${inbound.teamId}:${inbound.channelId}:${inbound.rootTs}:${actorId}:${inbound.eventId}:${interactionId}`;
@@ -1403,21 +1421,26 @@ export class SlackNotificationDaemon {
1403
1421
  actorId,
1404
1422
  retryKey,
1405
1423
  eventContext: inbound.eventContext,
1406
- kind: command ? "command" : "action",
1407
- ...(command ? {} : record.pendingActionId ? { actionId: record.pendingActionId } : {}),
1424
+ kind,
1425
+ ...(actionId ? { actionId } : {}),
1408
1426
  };
1409
- const initialPayload: SlackInboundEffectPayload | undefined = command
1410
- ? { type: "command", content: inboundText!, idempotencyKey, routing }
1411
- : record.pendingActionId
1412
- ? { type: "reply", id: record.pendingActionId, answer: inboundText ?? "", idempotencyKey, routing }
1413
- : undefined;
1427
+ const initialPayload: SlackInboundEffectPayload = command
1428
+ ? { type: "command", content: inboundText, idempotencyKey, routing }
1429
+ : actionId
1430
+ ? { type: "reply", id: actionId, answer: inboundText, idempotencyKey, routing }
1431
+ : { type: "user_message", sessionId: record.sessionId, text: inboundText, idempotencyKey, routing };
1414
1432
  const existingEffect = await this.#journal.read<SlackInboundEffectPayload>(effectId);
1415
1433
  const payload = existingEffect?.payload ?? initialPayload;
1416
- if (!payload) return undefined;
1434
+ const dispatchKind = payload.routing.kind;
1417
1435
  await this.#rescheduleAfterEffectTransition(
1418
1436
  this.#journal.enqueue({
1419
1437
  id: effectId,
1420
- kind: command ? "sdk.inbound.command" : "sdk.inbound.reply",
1438
+ kind:
1439
+ dispatchKind === "command"
1440
+ ? "sdk.inbound.command"
1441
+ : dispatchKind === "action"
1442
+ ? "sdk.inbound.reply"
1443
+ : "sdk.inbound.user_message",
1421
1444
  transport: "slack",
1422
1445
  sessionId: record.sessionId,
1423
1446
  endpointGeneration: endpoint.generation,
@@ -1437,8 +1460,7 @@ export class SlackNotificationDaemon {
1437
1460
  candidate =>
1438
1461
  candidate.eventId === inbound.eventId ||
1439
1462
  candidate.interactionId === interactionId ||
1440
- candidate.retryKey === retryKey ||
1441
- (inbound.eventContext !== undefined && candidate.eventContext === inbound.eventContext),
1463
+ candidate.retryKey === retryKey,
1442
1464
  );
1443
1465
  if (existing) {
1444
1466
  sessionId = current.sessionId;
@@ -1449,8 +1471,8 @@ export class SlackNotificationDaemon {
1449
1471
  current.seenEventIds.includes(inbound.eventId) ||
1450
1472
  current.seenInteractionIds.includes(interactionId) ||
1451
1473
  current.seenRetryKeys.includes(retryKey) ||
1452
- (inbound.eventContext !== undefined && current.seenContextIds.includes(inbound.eventContext)) ||
1453
- (!command && !current.pendingActionId)
1474
+ (dispatchKind === "action" && replyableActionId(current.pendingActionId) !== payload.routing.actionId) ||
1475
+ (dispatchKind === "message" && replyableActionId(current.pendingActionId) !== undefined)
1454
1476
  )
1455
1477
  return current;
1456
1478
  sessionId = current.sessionId;
@@ -1460,8 +1482,8 @@ export class SlackNotificationDaemon {
1460
1482
  interactionId,
1461
1483
  retryKey,
1462
1484
  eventContext: inbound.eventContext,
1463
- kind: command ? "command" : "action",
1464
- ...(command ? {} : { actionId: current.pendingActionId }),
1485
+ kind: dispatchKind,
1486
+ ...(dispatchKind === "action" ? { actionId: payload.routing.actionId } : {}),
1465
1487
  endpointGeneration: endpoint.generation,
1466
1488
  ...(current.attachmentAuthorityId === undefined
1467
1489
  ? {}
@@ -1747,7 +1769,9 @@ export class SlackNotificationDaemon {
1747
1769
  effect.transport !== "slack" ||
1748
1770
  effect.state === "terminal" ||
1749
1771
  !effect.sessionId ||
1750
- (effect.kind !== "sdk.inbound.command" && effect.kind !== "sdk.inbound.reply")
1772
+ (effect.kind !== "sdk.inbound.command" &&
1773
+ effect.kind !== "sdk.inbound.reply" &&
1774
+ effect.kind !== "sdk.inbound.user_message")
1751
1775
  )
1752
1776
  continue;
1753
1777
  const adopted = await this.#adoptOrphanInbound(effect as ChatEffect<SlackInboundEffectPayload>);
@@ -1794,7 +1818,9 @@ export class SlackNotificationDaemon {
1794
1818
  text(routing.interactionId) !== undefined &&
1795
1819
  routing.retryKey === `${routing.eventId}:${routing.interactionId}` &&
1796
1820
  (routing.eventContext === undefined || text(routing.eventContext) !== undefined) &&
1797
- (routing.kind === "command" || (routing.kind === "action" && text(routing.actionId) !== undefined))
1821
+ (routing.kind === "command" ||
1822
+ routing.kind === "message" ||
1823
+ (routing.kind === "action" && text(routing.actionId) !== undefined))
1798
1824
  );
1799
1825
  }
1800
1826
  #matchesInboundEffect(effect: ChatEffect<SlackInboundEffectPayload>, receipt: SlackInboundDispatchReceipt): boolean {
@@ -1828,7 +1854,11 @@ export class SlackNotificationDaemon {
1828
1854
  (payload.type === "reply" &&
1829
1855
  routing.kind === "action" &&
1830
1856
  effect.kind === "sdk.inbound.reply" &&
1831
- payload.id === routing.actionId))
1857
+ payload.id === routing.actionId) ||
1858
+ (payload.type === "user_message" &&
1859
+ routing.kind === "message" &&
1860
+ effect.kind === "sdk.inbound.user_message" &&
1861
+ payload.sessionId === effect.sessionId))
1832
1862
  );
1833
1863
  }
1834
1864
  #sameInboundReceipt(left: SlackInboundDispatchReceipt, right: SlackInboundDispatchReceipt): boolean {
@@ -1868,7 +1898,9 @@ export class SlackNotificationDaemon {
1868
1898
  if (
1869
1899
  effect.transport !== "slack" ||
1870
1900
  effect.state !== "terminal" ||
1871
- (effect.kind !== "sdk.inbound.command" && effect.kind !== "sdk.inbound.reply")
1901
+ (effect.kind !== "sdk.inbound.command" &&
1902
+ effect.kind !== "sdk.inbound.reply" &&
1903
+ effect.kind !== "sdk.inbound.user_message")
1872
1904
  )
1873
1905
  continue;
1874
1906
  const payload = effect.payload as SlackInboundEffectPayload;
@@ -1958,7 +1990,11 @@ export class SlackNotificationDaemon {
1958
1990
  (payload.type === "reply" &&
1959
1991
  routing.kind === "action" &&
1960
1992
  effect.kind === "sdk.inbound.reply" &&
1961
- payload.id === routing.actionId));
1993
+ payload.id === routing.actionId) ||
1994
+ (payload.type === "user_message" &&
1995
+ routing.kind === "message" &&
1996
+ effect.kind === "sdk.inbound.user_message" &&
1997
+ payload.sessionId === effect.sessionId));
1962
1998
  let receipt: SlackInboundDispatchReceipt | undefined;
1963
1999
  await this.store.transact(key, current => {
1964
2000
  if (
@@ -1974,8 +2010,7 @@ export class SlackNotificationDaemon {
1974
2010
  candidate =>
1975
2011
  candidate.eventId === routing.eventId ||
1976
2012
  candidate.interactionId === routing.interactionId ||
1977
- candidate.retryKey === routing.retryKey ||
1978
- (routing.eventContext !== undefined && candidate.eventContext === routing.eventContext),
2013
+ candidate.retryKey === routing.retryKey,
1979
2014
  );
1980
2015
  if (existing) {
1981
2016
  if (this.#matchesInboundEffect(effect, existing)) receipt = existing;
@@ -1985,8 +2020,7 @@ export class SlackNotificationDaemon {
1985
2020
  current.seenEventIds.includes(routing.eventId) ||
1986
2021
  current.seenInteractionIds.includes(routing.interactionId) ||
1987
2022
  current.seenRetryKeys.includes(routing.retryKey) ||
1988
- (routing.eventContext !== undefined && current.seenContextIds.includes(routing.eventContext)) ||
1989
- (routing.kind === "action" && current.pendingActionId !== routing.actionId)
2023
+ (routing.kind === "action" && replyableActionId(current.pendingActionId) !== routing.actionId)
1990
2024
  )
1991
2025
  return current;
1992
2026
  receipt = {
@@ -291,7 +291,7 @@ export const SDK_LIFECYCLE_ROUTER_PROTOCOL_VERSION = 1;
291
291
  * killing a session's mirroring after one transient rejection and must be
292
292
  * replaced across this upgrade.
293
293
  */
294
- export const DAEMON_GENERATION = 180;
294
+ export const DAEMON_GENERATION = 181;
295
295
 
296
296
  /**
297
297
  * Serving-compatibility boundary for daemon lifecycle requests. Epoch 7
@@ -41,16 +41,20 @@ export interface SdkSessionRowV1 {
41
41
  ambiguous?: boolean;
42
42
  }
43
43
 
44
- export interface SdkCheckpointRecordV1 {
44
+ export interface SdkRingPositionV1 {
45
45
  revision: number;
46
46
  generation: number;
47
47
  seq: number;
48
48
  }
49
49
 
50
+ export interface SdkCheckpointRecordV1 extends SdkRingPositionV1 {
51
+ idle: boolean;
52
+ }
53
+
50
54
  export interface SdkRetentionGapV1 {
51
55
  code: "retention_gap";
52
56
  missing?: { from: number; to: number };
53
- resync?: SdkCheckpointRecordV1;
57
+ resync?: SdkRingPositionV1;
54
58
  }
55
59
 
56
60
  /** One tail item: a retained transcript entry or a live event-ring event. */
@@ -58,6 +62,8 @@ export interface SdkTailItemV1 {
58
62
  /** "transcript" for retained transcript entries, otherwise the event-ring kind. */
59
63
  kind: string;
60
64
  id?: string;
65
+ /** Durable revision at emission; (revision,generation,seq) is unique per session. generation:seq alone is revision-local. */
66
+ revision?: number;
61
67
  generation?: number;
62
68
  seq?: number;
63
69
  payload: unknown;
@@ -72,6 +78,43 @@ export interface SdkTailEnvelopeV1 {
72
78
  items: SdkTailItemV1[];
73
79
  }
74
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
+
75
118
  const SECRET_FIELD = /(?:secret|token|password|credential|authorization|api[_-]?key)/i;
76
119
 
77
120
  /** Recursively removes secret-shaped fields (defense in depth for DTO output). */
@@ -165,6 +208,7 @@ export function toCheckpointRecordV1(value: unknown): SdkCheckpointRecordV1 | un
165
208
  const revision = value.revision;
166
209
  const generation = value.generation;
167
210
  const seq = value.seq;
211
+ const idle = value.idle;
168
212
  if (
169
213
  typeof revision !== "number" ||
170
214
  !Number.isSafeInteger(revision) ||
@@ -174,10 +218,11 @@ export function toCheckpointRecordV1(value: unknown): SdkCheckpointRecordV1 | un
174
218
  generation < 0 ||
175
219
  typeof seq !== "number" ||
176
220
  !Number.isSafeInteger(seq) ||
177
- seq < 0
221
+ seq < 0 ||
222
+ typeof idle !== "boolean"
178
223
  )
179
224
  return undefined;
180
- return { revision, generation, seq };
225
+ return { revision, generation, seq, idle };
181
226
  }
182
227
 
183
228
  export function toRetentionGapV1(value: unknown): SdkRetentionGapV1 | undefined {
@@ -189,15 +234,28 @@ export function toRetentionGapV1(value: unknown): SdkRetentionGapV1 | undefined
189
234
  if (typeof from === "number" && Number.isSafeInteger(from) && typeof to === "number" && Number.isSafeInteger(to))
190
235
  gap.missing = { from, to };
191
236
  }
192
- const resync = toCheckpointRecordV1(value.resync);
193
- if (resync !== undefined) gap.resync = resync;
237
+ if (isRecord(value.resync)) {
238
+ const { revision, generation, seq } = value.resync;
239
+ if (
240
+ typeof revision === "number" &&
241
+ Number.isSafeInteger(revision) &&
242
+ revision >= 0 &&
243
+ typeof generation === "number" &&
244
+ Number.isSafeInteger(generation) &&
245
+ generation >= 0 &&
246
+ typeof seq === "number" &&
247
+ Number.isSafeInteger(seq) &&
248
+ seq >= 0
249
+ )
250
+ gap.resync = { revision, generation, seq };
251
+ }
194
252
  return gap;
195
253
  }
196
254
 
197
255
  /** Projects a transcript entry or ring event into a tail item with dedupe keys. */
198
256
  export function toTailItemV1(
199
257
  value: unknown,
200
- fallback: { kind: string; generation?: number; seq?: number },
258
+ fallback: { kind: string; revision?: number; generation?: number; seq?: number },
201
259
  ): SdkTailItemV1 {
202
260
  if (!isRecord(value)) return { kind: fallback.kind, payload: stripSecretFields(value) };
203
261
  const kind = optionalString(value.kind) ?? fallback.kind;
@@ -207,10 +265,20 @@ export function toTailItemV1(
207
265
  const item: SdkTailItemV1 = {
208
266
  kind,
209
267
  ...(payloadId !== undefined ? { id: payloadId } : id !== undefined ? { id } : {}),
268
+ ...(typeof value.revision === "number" && Number.isSafeInteger(value.revision) && value.revision >= 0
269
+ ? { revision: value.revision }
270
+ : {}),
210
271
  ...(typeof value.generation === "number" ? { generation: value.generation } : {}),
211
272
  ...(typeof value.seq === "number" ? { seq: value.seq } : {}),
212
273
  payload: stripSecretFields(payload),
213
274
  };
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;
214
282
  if (fallback.generation !== undefined && item.generation === undefined) item.generation = fallback.generation;
215
283
  if (fallback.seq !== undefined && item.seq === undefined) item.seq = fallback.seq;
216
284
  return item;