dsh-acp-enhanced 0.5.0 → 0.5.2

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 (2) hide show
  1. package/lib/index.js +53 -14
  2. package/package.json +1 -1
package/lib/index.js CHANGED
@@ -446,8 +446,12 @@ export function apply(ctx, config) {
446
446
  /** Whether one live session has produced anything yet. A preset swap is only
447
447
  * legal while it is blank (dsh-agent-presets product rule): swapping tools
448
448
  * mid-conversation would strand logged tool calls the new composition cannot
449
- * make. */
450
- const isBlankSession = (session) => !session.events.some((event) => event.type === 'turn/start')
449
+ * make. Checks both the live turn marker and persisted user messages, so the
450
+ * same rule holds for resumed sessions whose on-disk logs may not carry
451
+ * `turn/start`. */
452
+ const isBlankSession = (session) => !session.events.some((event) => (
453
+ event.type === 'turn/start' || event.type === 'user/message'
454
+ ))
451
455
 
452
456
  /**
453
457
  * Resolve the preset a new/resumed session will run under, and the setup hook
@@ -476,13 +480,21 @@ export function apply(ctx, config) {
476
480
  }
477
481
  }
478
482
 
479
- /** The preset a persisted session runs, or undefined when unrecorded. */
480
- async function persistedPresetOf(sessionId) {
483
+ /** Persisted facts for one stored session: the preset it runs under and
484
+ * whether its log shows any real content. Both read from the on-disk log
485
+ * because a just-resumed session's in-memory event log fills asynchronously
486
+ * — the loadSession response would otherwise judge blank-ness before the
487
+ * events arrive. Returns undefined when the artifact is missing/corrupt
488
+ * (resume itself reports the failure; this lookup must not mask it). */
489
+ async function persistedFactsOf(sessionId) {
481
490
  const persistence = ctx.get('sessionPersistence')
482
491
  if (persistence === undefined) return undefined
483
492
  try {
484
493
  const { meta, events } = await persistence.load(sessionId)
485
- return resolveSessionPreset({ header: meta, events })
494
+ return {
495
+ preset: resolveSessionPreset({ header: meta, events }),
496
+ blank: isBlankSession({ header: meta, events }),
497
+ }
486
498
  } catch {
487
499
  // A missing/corrupt artifact leaves resume itself to report the failure;
488
500
  // the preset lookup must not mask it with a second, misleading error.
@@ -710,6 +722,16 @@ export function apply(ctx, config) {
710
722
  case 'turn/start': {
711
723
  record.turnCount += 1
712
724
  record.turnStartedAt = Date.now()
725
+ // The preset-selection window closes the moment the session produces
726
+ // its first turn: re-advertise the config so the editor's
727
+ // agent_preset dropdown collapses to the running preset (ACP has no
728
+ // disabled state; without this the client keeps showing the full
729
+ // session/new list and offers switches the server must reject).
730
+ if (record.turnCount === 1) {
731
+ broadcastConfig(record).catch((error) => {
732
+ logger.warn(`acp-enhanced: config rebroadcast after first turn failed: ${String(error)}`)
733
+ })
734
+ }
713
735
  break
714
736
  }
715
737
  case 'turn/end': {
@@ -1090,18 +1112,30 @@ export function apply(ctx, config) {
1090
1112
  // Broken presets must not be offered: mounting one always fails.
1091
1113
  const mountable = (await presets.list()).filter((preset) => preset.broken === undefined)
1092
1114
  if (mountable.length > 0) {
1115
+ const current = runningPresetOf(record.agent.session) ?? ''
1116
+ // A just-resumed session's in-memory events fill asynchronously; trust
1117
+ // the on-disk log snapshot taken at load time when one exists.
1118
+ const blank = record.blankFromLog ?? isBlankSession(record.agent.session)
1093
1119
  options.push({
1094
1120
  id: 'agent_preset',
1095
1121
  type: 'select',
1096
1122
  name: 'Agent preset',
1097
- description: 'Model-facing tool/prompt composition for this session (switch only while blank).',
1123
+ description: blank
1124
+ ? 'Model-facing tool/prompt composition for this session (switch only while blank).'
1125
+ : 'Model-facing tool/prompt composition for this session (locked: switching requires a blank session).',
1098
1126
  category: 'model_config',
1099
- currentValue: runningPresetOf(record.agent.session) ?? '',
1100
- options: mountable.map((preset) => ({
1101
- value: preset.id,
1102
- name: preset.name ?? preset.id,
1103
- ...preset.description === undefined ? {} : { description: preset.description },
1104
- })),
1127
+ currentValue: current,
1128
+ // ACP has no per-option disabled state, so a non-blank session
1129
+ // advertises only the running preset — the editor shows the current
1130
+ // mode without offering a switch the server would reject (the
1131
+ // setSessionConfigOption guard stays as the authoritative check).
1132
+ options: blank
1133
+ ? mountable.map((preset) => ({
1134
+ value: preset.id,
1135
+ name: preset.name ?? preset.id,
1136
+ ...preset.description === undefined ? {} : { description: preset.description },
1137
+ }))
1138
+ : [{ value: current, name: mountable.find((preset) => preset.id === current)?.name ?? (current || 'host') }],
1105
1139
  })
1106
1140
  }
1107
1141
  }
@@ -1761,6 +1795,10 @@ export function apply(ctx, config) {
1761
1795
  buffer: {},
1762
1796
  thoughtBuffer: {},
1763
1797
  contextWindow: undefined,
1798
+ /** Blank-ness captured from the on-disk log at load time (resumed
1799
+ * sessions' in-memory events fill asynchronously). Undefined for
1800
+ * fresh/live sessions, which judge blank-ness live. */
1801
+ blankFromLog: undefined,
1764
1802
  lastUsage: undefined,
1765
1803
  selection,
1766
1804
  }
@@ -2021,10 +2059,10 @@ export function apply(ctx, config) {
2021
2059
  configOptions: await buildConfigOptions(live),
2022
2060
  }
2023
2061
  }
2024
- const runningPreset = await persistedPresetOf(sessionId)
2062
+ const runningPreset = await persistedFactsOf(sessionId)
2025
2063
  let composition
2026
2064
  try {
2027
- composition = await composePreset(runningPreset)
2065
+ composition = await composePreset(runningPreset?.preset)
2028
2066
  } catch (error) {
2029
2067
  // Same mapping as session/new: a roster that cannot supply the
2030
2068
  // session's logged preset is reported as a client mistake.
@@ -2048,6 +2086,7 @@ export function apply(ctx, config) {
2048
2086
  }
2049
2087
  const record = makeRecord(handle)
2050
2088
  record.additionalDirectories = additionalDirectories
2089
+ record.blankFromLog = runningPreset?.blank
2051
2090
  sessions.set(sessionId, record)
2052
2091
  // Zed inserts the thread before the load RPC completes; replay the
2053
2092
  // conversation history as notifications so the thread renders.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-acp-enhanced",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Enhanced ACP server for DeepSeek Harness: block-level streaming, usage/stat telemetry (cache hit rate, token speed, input/output tokens, context length, turns, tool timing), model & reasoning-effort switching, and permission-preset control over the ACP wire (Zed-friendly)",
5
5
  "keywords": [
6
6
  "dsh",