2026-07-30 17:40:25 +08:00
/ * *
* Human - facing ` /compact ` command over the backend - independent compaction seam .
* @module @deepseek - ai / dsh - command - compact
* /
build(vendor): rescope the vendored Cordis packages into @deepseek-ai
Machine-produced by `pnpm run rescope-vendor --apply` plus the regeneration it
prints: `pnpm install` for the lockfile, `pnpm run gen-third-party-notices`,
`verify-translation-pairing --write` for the touched bilingual pairs,
`gen-doc-graphs`, and one typert snapshot whose ids embed character offsets.
`pnpm run rescope-vendor --check` verifies the result.
Renames nine vendored packages (cordis, cosmokit, schemastery and the six
@cordisjs plugins) and every reference that resolves them: manifest names and
dependency keys, module specifiers including declare-module merges, cordis.yml
plugin names, tsconfig paths, every Markdown fence, and `docs/` prose.
Directory names, upstream versions, and dependency ranges are unchanged, so
vendor/README.md still reads as an upstream snapshot; its manifest table gains
an upstream-name column so THIRD_PARTY_NOTICES keeps MIT attribution pointed
at each fork's origin.
The tutorial tier follows the rename end to end: its yaml fences named plugins
the Loader can no longer resolve, its `ts ignore-check` fences disagreed with
the compiled fences beside them, and its prose quoted both. The contracts that
told readers to keep upstream names — the root convention and the vendoring
cookbook's tree comment and manifest invariant — now say to rescope instead.
Two rules read `@deepseek-ai/` as "another workspace plugin": the client bundle
purity gate now names the vendored libraries a browser bundle inlines, and the
files where a bare `cordis` is an agent-preset id keep that product data.
2026-08-10 22:04:06 +08:00
import type { Context } from '@deepseek-ai/cordis'
2026-08-13 00:36:22 +08:00
import { ManualCompactionError } from '@deepseek-ai/dsh-compaction'
2026-07-30 17:40:25 +08:00
import type { CommandInvocation , CommandResult } from '@deepseek-ai/dsh-commands'
export const name = 'command-compact'
2026-08-13 00:36:22 +08:00
export const inject = [ 'commands' , 'compaction' ]
2026-07-30 17:40:25 +08:00
const USAGE = 'Usage: /compact (no arguments)'
/** Fail loudly if a locally closed union gains an unhandled member. */
/* v8 ignore start -- closed-union backstop is unreachable without violating the TypeScript contract */
function assertNever ( value : never ) : never {
throw new TypeError ( ` unknown manual compaction error code: ${ String ( value ) } ` )
}
/* v8 ignore stop */
/** Convert expected capability failures into concise human-only outcomes. */
function expectedFailure ( error : ManualCompactionError ) : CommandResult {
switch ( error . code ) {
case 'busy' :
return {
kind : 'error' ,
text : 'Compaction is unavailable because this process has an active compaction, or the agent is not idle.' ,
}
2026-08-03 10:53:08 +08:00
case 'cancelled' :
return { kind : 'error' , text : 'Compaction cancelled.' }
2026-07-30 17:40:25 +08:00
case 'changed' :
return {
kind : 'error' ,
text : 'The history selected for compaction changed before it could be replaced. The conversation is unchanged; the attempt is recorded in the session log.' ,
}
case 'summary' :
return {
kind : 'error' ,
text : 'Compaction could not produce a useful summary. The conversation is unchanged; the attempt is recorded in the session log.' ,
}
case 'commit' :
return {
kind : 'error' ,
text : 'Compaction did not finish cleanly; some session history may have changed. Inspect the current session state before retrying.' ,
}
case 'persistence' :
return {
kind : 'error' ,
text : 'Compaction finished, but the session could not be saved.' ,
}
/* v8 ignore next 2 -- ManualCompactionErrorCode is closed and every member is handled above */
default : return assertNever ( error . code )
}
}
/** Execute one argument-free manual compaction request. */
async function executeCompact (
ctx : Context ,
invocation : CommandInvocation ,
) : Promise < CommandResult > {
if ( invocation . rawInput . trim ( ) . length > 0 ) {
return { kind : 'error' , text : USAGE }
}
try {
2026-08-13 00:36:22 +08:00
const result = await ctx . compaction . compactNow ( invocation . agent , invocation . signal , invocation . commandId )
2026-07-30 17:40:25 +08:00
if ( result === null ) return { kind : 'success' , text : 'No compactable history yet.' }
return {
kind : 'success' ,
text : ` Compacted ${ result . shadowedSeqs . length } history items (~ ${ result . shadowedTokenCount } tokens). ` ,
2026-08-08 14:11:17 +08:00
sourceEventSeq : result.summarySeq ,
2026-07-30 17:40:25 +08:00
}
} catch ( error : unknown ) {
if ( invocation . signal . aborted ) return { kind : 'error' , text : 'Compaction cancelled.' }
if ( error instanceof ManualCompactionError ) return expectedFailure ( error )
throw error
}
}
/ * *
* Register ` /compact ` for every composed human - command adapter .
* @param ctx - context carrying the command registry and the compaction seam .
* /
export function apply ( ctx : Context ) : void {
2026-07-30 22:00:34 +08:00
const active = new Set < Promise < CommandResult > > ( )
const handler = ( invocation : CommandInvocation ) : Promise < CommandResult > = > {
const operation = executeCompact ( ctx , invocation )
active . add ( operation )
const retire = ( ) : void = > { active . delete ( operation ) }
// Both branches retire without rethrowing, so the derived observer promise
// cannot become an unhandled mirror of an expected handler rejection.
void operation . then ( retire , retire )
return operation
}
ctx . effect ( function * ( ) {
// Yield drain before registration: composite teardown is LIFO, so no new
// invocation can enter while already-started handler promises quiesce.
yield async ( ) = > { await Promise . allSettled ( active ) }
yield ctx . commands . register ( {
name : 'compact' ,
description : 'Compact older conversation history' ,
handler ,
} )
} , 'command-compact lifecycle' )
2026-07-30 17:40:25 +08:00
}