index.ts 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
  1. /**
  2. * Human-facing `/compact` command over the backend-independent compaction seam.
  3. * @module @deepseek-ai/dsh-command-compact
  4. */
  5. import type { Context } from '@deepseek-ai/cordis'
  6. import { CommandDefinitionId } from '@deepseek-ai/dsh-commands/brand'
  7. import { ManualCompactionError } from '@deepseek-ai/dsh-compaction'
  8. import type { CommandInvocation, CommandResult } from '@deepseek-ai/dsh-commands'
  9. export const name = 'command-compact'
  10. export const inject = ['commands', 'compaction']
  11. const USAGE = 'Usage: /compact (no arguments)'
  12. /** Fail loudly if a locally closed union gains an unhandled member. */
  13. /* v8 ignore start -- closed-union backstop is unreachable without violating the TypeScript contract */
  14. function assertNever(value: never): never {
  15. throw new TypeError(`unknown manual compaction error code: ${String(value)}`)
  16. }
  17. /* v8 ignore stop */
  18. /** Convert expected capability failures into concise human-only outcomes. */
  19. function expectedFailure(error: ManualCompactionError): CommandResult {
  20. switch (error.code) {
  21. case 'busy':
  22. return {
  23. kind: 'error',
  24. text: 'Compaction is unavailable because this process has an active compaction, or the agent is not idle.',
  25. }
  26. case 'cancelled':
  27. return { kind: 'error', text: 'Compaction cancelled.' }
  28. case 'changed':
  29. return {
  30. kind: 'error',
  31. text: 'The history selected for compaction changed before it could be replaced. The attempt is recorded in the session log.',
  32. }
  33. case 'summary':
  34. return {
  35. kind: 'error',
  36. text: 'Compaction could not produce a useful summary. The attempt is recorded in the session log.',
  37. }
  38. case 'commit':
  39. return {
  40. kind: 'error',
  41. text: 'Compaction did not finish cleanly; some session history may have changed. Inspect the current session state before retrying.',
  42. }
  43. case 'persistence':
  44. return {
  45. kind: 'error',
  46. text: 'Compaction finished, but the session could not be saved.',
  47. }
  48. /* v8 ignore next 2 -- ManualCompactionErrorCode is closed and every member is handled above */
  49. default: return assertNever(error.code)
  50. }
  51. }
  52. /** Execute one argument-free manual compaction request. */
  53. async function executeCompact(
  54. ctx: Context,
  55. invocation: CommandInvocation,
  56. ): Promise<CommandResult> {
  57. if (invocation.rawInput.trim().length > 0) {
  58. return { kind: 'error', text: USAGE }
  59. }
  60. try {
  61. const result = await ctx.compaction.compactNow(invocation.agent, invocation.signal, invocation.commandId)
  62. if (result === null) return { kind: 'success', text: 'No compactable history yet.' }
  63. return {
  64. kind: 'success',
  65. text: `Compacted ${result.shadowedSeqs.length} history items (~${result.shadowedTokenCount} tokens).`,
  66. sourceEventSeq: result.summarySeq,
  67. }
  68. } catch (error: unknown) {
  69. if (invocation.signal.aborted) return { kind: 'error', text: 'Compaction cancelled.' }
  70. if (error instanceof ManualCompactionError) return expectedFailure(error)
  71. throw error
  72. }
  73. }
  74. /**
  75. * Register `/compact` for every composed human-command adapter.
  76. * @param ctx - context carrying the command registry and the compaction seam.
  77. */
  78. export function apply(ctx: Context): void {
  79. const active = new Set<Promise<CommandResult>>()
  80. const handler = (invocation: CommandInvocation): Promise<CommandResult> => {
  81. const operation = executeCompact(ctx, invocation)
  82. active.add(operation)
  83. const retire = (): void => { active.delete(operation) }
  84. // Both branches retire without rethrowing, so the derived observer promise
  85. // cannot become an unhandled mirror of an expected handler rejection.
  86. void operation.then(retire, retire)
  87. return operation
  88. }
  89. ctx.effect(function* () {
  90. // Yield drain before registration: composite teardown is LIFO, so no new
  91. // invocation can enter while already-started handler promises quiesce.
  92. yield async () => { await Promise.allSettled(active) }
  93. yield ctx.commands.register({
  94. definitionId: CommandDefinitionId('@deepseek-ai/dsh-command-compact'),
  95. name: 'compact',
  96. description: 'Compact older conversation history',
  97. handler,
  98. })
  99. }, 'command-compact lifecycle')
  100. }