dsh-remote 0.8.66 → 0.8.70

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.
@@ -29,7 +29,8 @@
29
29
  // and every caller is expected to treat a rejection as "no injection".
30
30
 
31
31
  import { statSync, mkdirSync, writeFileSync, utimesSync } from 'node:fs'
32
- import { createHash } from 'node:crypto'
32
+ import { createHash, randomUUID } from 'node:crypto'
33
+ import { createRequire } from 'node:module'
33
34
  import path from 'node:path'
34
35
  import { joinRemotePath, relPathUnder } from './paths.js'
35
36
  import { INSTRUCTION_MAX_BYTES } from './instructions.js'
@@ -39,6 +40,77 @@ import { INSTRUCTION_MAX_BYTES } from './instructions.js'
39
40
  * reconcile only claims messages carrying its own kind. */
40
41
  export const REMOTE_INSTRUCTIONS_KIND = 'dsh-remote-instructions'
41
42
 
43
+ /**
44
+ * The user-message factory hanging off the `llm` service, when that service
45
+ * actually exposes one.
46
+ *
47
+ * DSH 0.2.1 keeps `createUserMessage` as a module export of
48
+ * `@deepseek-ai/dsh-llm`, not a method of the `LlmRuntime` service. Probing
49
+ * `ctx.get('llm')` therefore returns null on current Desktop builds, and the
50
+ * caller must fall through to {@link loadBundledUserMessageFactory}.
51
+ *
52
+ * @param {object|null|undefined} llm - `ctx.get('llm')`, or a test double.
53
+ * @returns {Function|null}
54
+ */
55
+ export function pickUserMessageFactory(llm) {
56
+ if (llm && typeof llm.createUserMessage === 'function') {
57
+ return (input) => llm.createUserMessage(input)
58
+ }
59
+ const nested = llm && llm.default
60
+ if (nested && typeof nested.createUserMessage === 'function') {
61
+ return (input) => nested.createUserMessage(input)
62
+ }
63
+ return null
64
+ }
65
+
66
+ /** Module-level `createUserMessage` from the harness's dsh-llm, if resolvable. */
67
+ let bundledUserMessageFactory
68
+ /**
69
+ * Load `@deepseek-ai/dsh-llm`'s `createUserMessage`.
70
+ *
71
+ * The package is not a declared dependency of this plugin (the compatibility
72
+ * gate would then have to peer-range it). Inside a DSH profile it is a sibling
73
+ * in `node_modules`, and Node walks up from this file to find it. A missing
74
+ * package returns null so the built-in identified message is used instead.
75
+ *
76
+ * @returns {Function|null}
77
+ */
78
+ export function loadBundledUserMessageFactory() {
79
+ if (bundledUserMessageFactory !== undefined) return bundledUserMessageFactory
80
+ try {
81
+ const req = createRequire(import.meta.url)
82
+ const mod = req('@deepseek-ai/dsh-llm')
83
+ bundledUserMessageFactory = mod && typeof mod.createUserMessage === 'function'
84
+ ? mod.createUserMessage
85
+ : null
86
+ } catch {
87
+ bundledUserMessageFactory = null
88
+ }
89
+ return bundledUserMessageFactory
90
+ }
91
+
92
+ /**
93
+ * Guarantee the session-restore invariant: `message.id` is a non-empty string.
94
+ *
95
+ * DSH 0.2.1 (`dsh-session` `assertMessageEventShape`) rejects the whole log
96
+ * with "lacks an identified message" when any user message has no id. A
97
+ * factory that already stamped one is returned unchanged.
98
+ *
99
+ * @param {object|null} message - a factory result, or null to use `fallback`.
100
+ * @param {object} fallback - `{ role, content, source }` used when `message` is empty.
101
+ * @returns {object}
102
+ */
103
+ export function ensureMessageId(message, fallback) {
104
+ const base = message && typeof message === 'object' ? message : fallback
105
+ if (typeof base.id === 'string' && base.id !== '') return base
106
+ return {
107
+ ...fallback,
108
+ ...base,
109
+ role: base.role || fallback.role,
110
+ id: randomUUID(),
111
+ }
112
+ }
113
+
42
114
  /** Total byte budget for one injected batch (the core's own budget is similar). */
43
115
  export const INSTRUCTION_MESSAGE_MAX_BYTES = 16 * 1024
44
116
 
@@ -332,5 +404,12 @@ export function buildInstructionMessage(items, removed, opts = {}) {
332
404
  ].join('\n')
333
405
  const source = { kind: REMOTE_INSTRUCTIONS_KIND, form: 'instructions', changes: sent.map((rel) => ({ path: rel, change: 'changed' })) }
334
406
  const content = [{ type: 'text', text }]
335
- return factory ? factory({ content, source }) : { role: 'user', content, source }
407
+ const fallback = { role: 'user', content, source }
408
+ let built = null
409
+ if (factory) {
410
+ try { built = factory({ content, source }) } catch { built = null }
411
+ }
412
+ // Session restore requires message.id (issue #75). A factory that throws or
413
+ // omits id must not store a bare object — that rejects the whole log.
414
+ return ensureMessageId(built, fallback)
336
415
  }