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.
- package/README.md +3 -24
- package/cordis.patch.yml +0 -32
- package/lib/client.js +0 -401
- package/package.json +3 -11
- package/lib/sessionlog/icon.svg +0 -19
- package/lib/sessionlog/index.js +0 -636
- package/lib/sessionlog/locale/en.json +0 -6
- package/lib/sessionlog/locale/zh.json +0 -6
- package/lib/sessionlog/package.json +0 -6
- package/lib/sessionlog/redact.js +0 -142
- package/lib/sessionlog/settings.js +0 -436
- package/lib/sessionlog/wire.js +0 -235
package/lib/sessionlog/wire.js
DELETED
|
@@ -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
|
-
}
|