dsh-workspace-plus 0.7.0 → 0.7.1

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.
@@ -1,235 +0,0 @@
1
- /**
2
- * dsh-workspace-plus / sessionlog — pure wire layer.
3
- *
4
- * Two serializations live here, both pure functions over a Session object, so
5
- * tools/verify.mjs can pin them against plain fixtures:
6
- *
7
- * 1. **OTLP records** — one record per canonical session event, in the exact
8
- * shape `ctx.otel.createSessionLogReporter(options).reportSessionLog(...)`
9
- * consumes. This is the provider-independent channel: it does not care
10
- * which model adapter is in play, which is the whole point of the
11
- * sub-feature.
12
- * 2. **`dsh_session_log` v1 request field** — byte-for-byte the format the
13
- * shipped `@deepseek-ai/dsh-session-log-deepseek` contributes, so a host
14
- * that does not mount that package still gets the same wire contract.
15
- *
16
- * Nothing here imports anything but the sibling redactor: OTel severity numbers
17
- * are fixed integers and the numeric Session brands are type-level only, so
18
- * plain numbers are accepted by `snapshotEvents`/`seq` at runtime.
19
- *
20
- * Both serializations route `event.data` through {@link redactEvent} first, so
21
- * neither channel can leak a credential that a tool result happened to carry.
22
- *
23
- * @module dsh-workspace-plus/sessionlog/wire
24
- */
25
-
26
- import { redactEvent } from './redact.js'
27
-
28
- /**
29
- * OTel severity numbers (`@opentelemetry/api-logs` SeverityNumber). Inlined
30
- * because an externally installed bundle cannot resolve `@deepseek-ai/*` or
31
- * `@opentelemetry/*` from its own location, and these values are part of the
32
- * OTLP wire contract rather than of any implementation.
33
- */
34
- export const SEVERITY_NUMBER = Object.freeze({ info: 9, warn: 13, error: 17 })
35
-
36
- /** Session-log request field name, identical to the shipped package's. */
37
- export const REQUEST_FIELD = 'dsh_session_log'
38
-
39
- /** Request-field envelope version, identical to the shipped package's. */
40
- export const REQUEST_FIELD_VERSION = 1
41
-
42
- /** OTLP scope name reported for this sub-feature's records. */
43
- export const SCOPE_NAME = 'dsh-workspace-plus/sessionlog'
44
-
45
- /** OTLP event name, matching the shipped session-telemetry backend. */
46
- export const EVENT_NAME = 'session-log'
47
-
48
- /** Opaque body marker, matching the shipped session-telemetry backend. */
49
- export const EVENT_BODY = 'session-log'
50
-
51
- /**
52
- * Map one event's own outcome flag to a pre-baked severity, exactly as
53
- * `@deepseek-ai/dsh-session-telemetry` does, so alerting built on the shipped
54
- * channel keeps working when records arrive through this one.
55
- *
56
- * @param event - a canonical session event.
57
- * @returns `'error'` or `'info'`.
58
- */
59
- export function severityOf(event) {
60
- switch (event?.type) {
61
- case 'tool/result':
62
- return event.data?.message?.isError === true ? 'error' : 'info'
63
- case 'turn/end':
64
- return event.data?.reason?.kind === 'error' ? 'error' : 'info'
65
- default:
66
- return 'info'
67
- }
68
- }
69
-
70
- /**
71
- * The minimal identity attributes the shipped backend attaches: the event
72
- * envelope plus self-contained header facts.
73
- *
74
- * @param session - the owning Session.
75
- * @param event - the event being reported.
76
- * @returns a flat attribute object.
77
- */
78
- export function identityOf(session, event) {
79
- const attributes = {
80
- 'session.id': String(session.id),
81
- 'session.format_version': session.header?.version,
82
- 'event.type': event.type,
83
- 'event.seq': Number(event.seq),
84
- }
85
- const header = session.header ?? {}
86
- if (header.cwd !== undefined) attributes['session.cwd'] = header.cwd
87
- if (header.parentSession !== undefined) attributes['session.parent_id'] = String(header.parentSession)
88
- if (header.isSeeded === true) attributes['session.seed_length'] = Number(session.inheritedEventCount ?? 0)
89
- return attributes
90
- }
91
-
92
- /**
93
- * Build the record one canonical event is reported as. The event is redacted and
94
- * copied, so the reporter's serializer never sees a live reference into the log
95
- * and never sees a credential the tool output happened to carry.
96
- *
97
- * @param session - the owning Session.
98
- * @param event - the canonical event.
99
- * @returns the `reportSessionLog` payload.
100
- */
101
- export function sessionLogRecord(session, event) {
102
- const safe = redactEvent(event)
103
- const { data, ...envelope } = safe
104
- return {
105
- sessionId: String(session.id),
106
- event: { ...envelope, data },
107
- severityNumber: SEVERITY_NUMBER[severityOf(event)],
108
- attributes: identityOf(session, event),
109
- }
110
- }
111
-
112
- /** Translate logical Session metadata to the request field's raw header. */
113
- export function wireHeader(session) {
114
- const header = session.header ?? {}
115
- return {
116
- version: header.version,
117
- id: String(header.id),
118
- createdAt: header.createdAt,
119
- ...(header.cwd === undefined ? {} : { cwd: header.cwd }),
120
- ...(header.parentSession === undefined ? {} : { parentSession: String(header.parentSession) }),
121
- ...(header.isSeeded === true ? { seedLength: Number(session.inheritedEventCount ?? 0) } : {}),
122
- ...(header.origin === undefined ? {} : { origin: header.origin }),
123
- ...(header.delegationDepth === undefined ? {} : { delegationDepth: header.delegationDepth }),
124
- ...(header.agentPreset === undefined ? {} : { agentPreset: header.agentPreset }),
125
- }
126
- }
127
-
128
- /**
129
- * Translate one logical surface operation to its raw request form.
130
- *
131
- * Canonical surface events always carry an operation, so this mirrors the
132
- * shipped translation exactly. The `undefined` guard only exists so a
133
- * third-party plugin cannot turn an unexpected event into a thrown request
134
- * preparation — the field is omitted in that case rather than guessed at.
135
- *
136
- * @param op - the event's `surfaceOp`.
137
- * @returns the raw form, or `undefined` when the event carried none.
138
- */
139
- export function wireSurfaceOp(op) {
140
- if (op === undefined || op === null) return undefined
141
- return op === 'append' ? op : { op: 'replace', startSeq: Number(op.startSeq), endSeq: Number(op.endSeq) }
142
- }
143
-
144
- /** Translate one canonical event to its raw request form. */
145
- export function wireEvent(event) {
146
- const safe = redactEvent(event)
147
- const common = {
148
- seq: Number(safe.seq),
149
- time: safe.time,
150
- data: safe.data,
151
- ...(safe.ignorable === undefined ? {} : { ignorable: safe.ignorable }),
152
- }
153
- switch (safe.type) {
154
- case 'developer/message':
155
- case 'system/message':
156
- case 'user/message':
157
- case 'tool/result': {
158
- const surfaceOp = wireSurfaceOp(safe.surfaceOp)
159
- return {
160
- ...common,
161
- type: safe.type,
162
- ...(surfaceOp === undefined ? {} : { surfaceOp }),
163
- ...(safe.sourceEventSeqs === undefined ? {} : { sourceEventSeqs: safe.sourceEventSeqs.map(Number) }),
164
- }
165
- }
166
- case 'assistant/message': {
167
- const surfaceOp = wireSurfaceOp(safe.surfaceOp)
168
- return { ...common, type: safe.type, ...(surfaceOp === undefined ? {} : { surfaceOp }) }
169
- }
170
- default:
171
- if (safe.ignorable === true) {
172
- return {
173
- ...common,
174
- type: safe.type,
175
- ignorable: true,
176
- ...(safe.surfaceOp === undefined ? {} : { surfaceOp: safe.surfaceOp }),
177
- ...(safe.sourceEventSeqs === undefined ? {} : { sourceEventSeqs: safe.sourceEventSeqs }),
178
- }
179
- }
180
- return { ...common, type: safe.type }
181
- }
182
- }
183
-
184
- /**
185
- * UTF-8 length of one value's JSON text as the request body encodes it.
186
- *
187
- * @param value - any JSON-ish value.
188
- * @returns the byte count, or `Infinity` when it cannot be serialized at all
189
- * (which counts as exceeding every limit).
190
- */
191
- export function jsonBytes(value) {
192
- let text
193
- try {
194
- text = JSON.stringify(value)
195
- } catch {
196
- return Number.POSITIVE_INFINITY
197
- }
198
- return typeof text === 'string' ? Buffer.byteLength(text, 'utf8') : Number.POSITIVE_INFINITY
199
- }
200
-
201
- /**
202
- * Build the `dsh_session_log` value for one request: the longest contiguous run
203
- * of events after `afterSeq` whose complete serialized field fits `maxBytes`.
204
- *
205
- * The accounting is deliberately the shipped package's: the envelope is
206
- * measured with an empty `events` array and one digit of `throughSeq`, then
207
- * each event is measured as its JSON text plus one separator byte, and the
208
- * `throughSeq` digits are re-added. Anything that does not fit is left for the
209
- * next request.
210
- *
211
- * @param options - the session, the exclusive lower bound, and the byte ceiling.
212
- * @returns `{ value, throughSeq }`, or `undefined` when nothing fits.
213
- */
214
- export function buildRequestField({ session, afterSeq, maxBytes }) {
215
- const pending = typeof session.snapshotEvents === 'function' ? session.snapshotEvents(afterSeq + 1) : []
216
- const envelope = {
217
- version: REQUEST_FIELD_VERSION,
218
- sessionFormatVersion: session.header?.version,
219
- session: wireHeader(session),
220
- afterSeq: Number(afterSeq),
221
- }
222
- let bytes = jsonBytes({ ...envelope, throughSeq: 0, events: [] }) - 1
223
- const events = []
224
- for (const event of pending) {
225
- const wire = wireEvent(event)
226
- const next = bytes + (events.length === 0 ? 0 : 1) + jsonBytes(wire)
227
- const candidateBytes = next + String(event.seq).length
228
- if (candidateBytes > maxBytes) break
229
- bytes = next
230
- events.push(wire)
231
- }
232
- if (events.length === 0) return undefined
233
- const throughSeq = Number(pending[events.length - 1].seq)
234
- return { value: { ...envelope, throughSeq, events }, throughSeq }
235
- }