dsh-code 1.0.4 → 1.0.5

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.
@@ -6,7 +6,7 @@
6
6
  * @module @deepseek-ai/dsh-code/render/export
7
7
  */
8
8
 
9
- import { assertNever } from '@deepseek-ai/dsh-llm'
9
+ import { assertNever } from '@deepseek-ai/dsh-util-values'
10
10
  import { imageLabels, type TranscriptView } from './projection.ts'
11
11
 
12
12
  /**
@@ -32,12 +32,12 @@ export function buildExportMarkdown(view: TranscriptView, sessionId: string): st
32
32
  out.push('## user', '', entry.text, ...(imageLabels(entry.images) === '' ? [] : [imageLabels(entry.images)]), '')
33
33
  }
34
34
  break
35
- case 'assistant':
36
- if (entry.reasoning !== '') {
37
- out.push('<details><summary>thinking</summary>', '', entry.reasoning, '', '</details>', '')
38
- }
39
- out.push('## assistant', '', entry.text, '')
40
- break
35
+ case 'assistant':
36
+ if (entry.reasoning !== '') {
37
+ out.push('<details><summary>thinking</summary>', '', entry.reasoning, '', '</details>', '')
38
+ }
39
+ out.push('## assistant', '', entry.text, '')
40
+ break
41
41
  case 'tool':
42
42
  out.push(`### tool \`${entry.name}\``, '')
43
43
  if (entry.preview !== '') out.push(`- args: ${entry.preview}`)
@@ -8,7 +8,8 @@
8
8
  */
9
9
 
10
10
  import { boundContextSummary, type ContentBlock, type ImageBlock, type MessageId } from '@deepseek-ai/dsh-llm'
11
- import type { SessionEvent, TodoItem } from '@deepseek-ai/dsh-session'
11
+ import type { SessionEvent } from '@deepseek-ai/dsh-session'
12
+ import type { TodoItem } from '@deepseek-ai/dsh-tool-todo'
12
13
  import { graphemeWidth, splitGraphemes } from './width.ts'
13
14
  // Type-only imports merge the plugin-owned SessionEventMap variants
14
15
  // (agent/inbox/spliced, command/*, compaction/*, goal/change, llm/retry*,
@@ -16,9 +16,46 @@
16
16
  * @module @deepseek-ai/dsh-code/settings-file
17
17
  */
18
18
 
19
+ import { randomUUID } from 'node:crypto'
19
20
  import { mkdir, rename, writeFile } from 'node:fs/promises'
20
21
  import { dirname } from 'node:path'
21
22
 
23
+ /**
24
+ * Run one file operation with a bounded retry: one initial try plus at
25
+ * most `retries` more. Creating or replacing a file can fail transiently
26
+ * with EPERM/EACCES while an antivirus scanner or search indexer holds
27
+ * it — the standard graceful-fs remedy, not a workaround for a
28
+ * persistent permission problem. A save that still fails leaves its
29
+ * uniquely named temp file behind, so repeated crashed saves accumulate
30
+ * distinct leftovers rather than corrupting a shared one.
31
+ */
32
+ async function withTransientRetry(operation: () => Promise<void>, retries = 5): Promise<void> {
33
+ for (let attempt = 0; ; attempt += 1) {
34
+ try {
35
+ await operation()
36
+ return
37
+ } catch (error: unknown) {
38
+ const code = (error as NodeJS.ErrnoException).code
39
+ if (attempt >= retries || (code !== 'EPERM' && code !== 'EACCES')) throw error
40
+ await new Promise(resolve => setTimeout(resolve, 30 * (attempt + 1)))
41
+ }
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Write one file atomically: create the parent directory, write to a
47
+ * uniquely named temp file, and rename it into place. A crash midway
48
+ * can never leave a half-written document behind. Unique temp names
49
+ * keep concurrent writers (two terminals, two chains in one process)
50
+ * from sharing one temp path.
51
+ */
52
+ export async function writeFileAtomically(path: string, text: string): Promise<void> {
53
+ await mkdir(dirname(path), { recursive: true })
54
+ const temp = `${path}.${process.pid}.${randomUUID()}.tmp`
55
+ await withTransientRetry(() => writeFile(temp, text, 'utf8'))
56
+ await withTransientRetry(() => rename(temp, path))
57
+ }
58
+
22
59
  /** The serialized persistence surface; flush() is handed to the quit sequence. */
23
60
  export interface UserSettingsPersistence {
24
61
  /**
@@ -39,12 +76,7 @@ export function createUserSettingsPersistence(): UserSettingsPersistence {
39
76
  let chain: Promise<void> = Promise.resolve()
40
77
  return {
41
78
  save(path: string, text: string): Promise<void> {
42
- const write = chain.then(async () => {
43
- await mkdir(dirname(path), { recursive: true })
44
- const temp = `${path}.tmp`
45
- await writeFile(temp, text, 'utf8')
46
- await rename(temp, path)
47
- })
79
+ const write = chain.then(() => writeFileAtomically(path, text))
48
80
  // A failed write must not break the chain for later saves.
49
81
  chain = write.catch(() => {})
50
82
  return write