dsh-realtime-audio-ws 0.2.3 → 0.2.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.
@@ -231,6 +231,12 @@ window.__ModuleLoader__.load({ id: "dsh-realtime-audio-ws",
231
231
  const source = context.createMediaStreamSource(stream);
232
232
  const node = context.createScriptProcessor(exports.CAPTURE_BUFFER, 1, 1);
233
233
  node.onaudioprocess = (event) => {
234
+ // Every quantum ships, **silent ones included, and that is load-bearing rather than incidental**: the
235
+ // provider's session timeline advances with the audio this client sends, and a context append is
236
+ // placed only while that timeline is advancing — so a client that skipped silent blocks (an
237
+ // obvious-looking saving) would silently freeze the session and lose every append after it. Measured,
238
+ // with the mechanism, in `docs/usable-window.md`; guarded by the capture-continuity case in
239
+ // `tests/client.spec.ts`.
234
240
  const pcm16 = pcm16FromFloat32(event.inputBuffer.getChannelData(0));
235
241
  socket?.send(new Uint8Array(pcm16.buffer));
236
242
  };
@@ -227,6 +227,12 @@ function createAudioClient(deps) {
227
227
  const source = context.createMediaStreamSource(stream);
228
228
  const node = context.createScriptProcessor(exports.CAPTURE_BUFFER, 1, 1);
229
229
  node.onaudioprocess = (event) => {
230
+ // Every quantum ships, **silent ones included, and that is load-bearing rather than incidental**: the
231
+ // provider's session timeline advances with the audio this client sends, and a context append is
232
+ // placed only while that timeline is advancing — so a client that skipped silent blocks (an
233
+ // obvious-looking saving) would silently freeze the session and lose every append after it. Measured,
234
+ // with the mechanism, in `docs/usable-window.md`; guarded by the capture-continuity case in
235
+ // `tests/client.spec.ts`.
230
236
  const pcm16 = pcm16FromFloat32(event.inputBuffer.getChannelData(0));
231
237
  socket?.send(new Uint8Array(pcm16.buffer));
232
238
  };
package/lib/index.d.ts CHANGED
@@ -40,12 +40,14 @@ export declare const Config: Schema<Schemastery.ObjectS<NoInfer<{
40
40
  maxConnections: Schema<number, number, "defined">;
41
41
  openSessionOnConnect: Schema<boolean, boolean, "defined">;
42
42
  diagnosticsPath: Schema<string, string, "defined">;
43
+ journalPath: Schema<string, string, "defined">;
43
44
  }>>, Schemastery.ObjectT<NoInfer<{
44
45
  path: Schema<string, string, "defined">;
45
46
  maxFrameBytes: Schema<number, number, "defined">;
46
47
  maxConnections: Schema<number, number, "defined">;
47
48
  openSessionOnConnect: Schema<boolean, boolean, "defined">;
48
49
  diagnosticsPath: Schema<string, string, "defined">;
50
+ journalPath: Schema<string, string, "defined">;
49
51
  }>>, "plain">;
50
52
  export declare function apply(ctx: Context, config: RealtimeAudioWsConfig): void;
51
53
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,MAAM,MAAM,0BAA0B,CAAA;AAC7C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAkBlD,OAAO,EAOL,KAAK,qBAAqB,EAC3B,MAAM,YAAY,CAAA;AAEnB,cAAc,YAAY,CAAA;AAC1B;;;;;;GAMG;AACH,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAC5C,OAAO,EAAE,iBAAiB,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,qBAAqB,EAAE,MAAM,aAAa,CAAA;AAC5F,OAAO,EACL,oBAAoB,EACpB,iBAAiB,EACjB,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,EACvB,KAAK,YAAY,EACjB,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,EACvB,KAAK,WAAW,GACjB,MAAM,cAAc,CAAA;AACrB,OAAO,EAAE,qBAAqB,EAAE,aAAa,EAAE,KAAK,eAAe,EAAE,MAAM,cAAc,CAAA;AACzF,OAAO,EAAE,gBAAgB,EAAE,KAAK,eAAe,EAAE,KAAK,kBAAkB,EAAE,KAAK,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AACzH,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AACrF,YAAY,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAEpE,mBAAmB;AACnB,eAAO,MAAM,IAAI,sBAAsB,CAAA;AAEvC,eAAO,MAAM,MAAM;;;;;;;;;;;;aASjB,CAAA;AAqCF,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,qBAAqB,GAAG,IAAI,CAqKvE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,MAAM,MAAM,0BAA0B,CAAA;AAC7C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAkBlD,OAAO,EAOL,KAAK,qBAAqB,EAC3B,MAAM,YAAY,CAAA;AAEnB,cAAc,YAAY,CAAA;AAC1B;;;;;;GAMG;AACH,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAC5C,OAAO,EAAE,iBAAiB,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,qBAAqB,EAAE,MAAM,aAAa,CAAA;AAC5F,OAAO,EACL,oBAAoB,EACpB,iBAAiB,EACjB,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,EACvB,KAAK,YAAY,EACjB,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,EACvB,KAAK,WAAW,GACjB,MAAM,cAAc,CAAA;AACrB,OAAO,EAAE,qBAAqB,EAAE,aAAa,EAAE,KAAK,eAAe,EAAE,MAAM,cAAc,CAAA;AACzF,OAAO,EAAE,gBAAgB,EAAE,KAAK,eAAe,EAAE,KAAK,kBAAkB,EAAE,KAAK,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AACzH,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AACrF,YAAY,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAIpE,mBAAmB;AACnB,eAAO,MAAM,IAAI,sBAAsB,CAAA;AAEvC,eAAO,MAAM,MAAM;;;;;;;;;;;;;;aAajB,CAAA;AAqCF,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,qBAAqB,GAAG,IAAI,CAiMvE"}
package/lib/index.js CHANGED
@@ -36,6 +36,7 @@ export { createControlHandler, parseControlFrame, } from './control.js';
36
36
  export { createUpgradeAcceptor, rejectUpgrade } from './upgrade.js';
37
37
  export { diagnosticsRoute } from './diagnostics.js';
38
38
  export { STRIP_ELEMENT_ID, stripBootstrap, stripMarkup, stripRows } from './strip.js';
39
+ import { createJournalFileSink } from './journal-file.js';
39
40
  /** Plugin name. */
40
41
  export const name = 'realtime-audio-ws';
41
42
  export const Config = Schema.object({
@@ -47,6 +48,10 @@ export const Config = Schema.object({
47
48
  // microphone: a connection takes an explicit action, an authenticated one, and its absence is silence.
48
49
  openSessionOnConnect: Schema.boolean().default(DEFAULT_OPEN_SESSION_ON_CONNECT),
49
50
  diagnosticsPath: Schema.string().default(DEFAULT_DIAGNOSTICS_PATH),
51
+ // Empty means no file, and empty is the default: a plugin that writes files because it was installed is a
52
+ // plugin writing files in somebody else's directory. The deployment that wants a recording is the
53
+ // deployment that names a path.
54
+ journalPath: Schema.string().default(''),
50
55
  });
51
56
  export function apply(ctx, config) {
52
57
  ctx.inject(['connection', 'webServer', 'realtime'], (injected) => {
@@ -64,6 +69,24 @@ export function apply(ctx, config) {
64
69
  // constructor argument: the plugin that mints the token is the only one that can name it, and it
65
70
  // mints it here, after the journal already exists.
66
71
  journal.addSecrets([token]);
72
+ // What this plugin resolved, so the journal can be read without the profile beside it. Values only, and
73
+ // no field that could carry a secret: `journalPath` names a file, and the token went in on the line above
74
+ // so that it can never reach a record in the first place.
75
+ journal.record('config.resolved', {
76
+ plugin: 'dsh-realtime-audio-ws',
77
+ path: config.path,
78
+ openSessionOnConnect: String(config.openSessionOnConnect),
79
+ maxConnections: String(config.maxConnections),
80
+ maxFrameBytes: String(config.maxFrameBytes),
81
+ journalPath: config.journalPath,
82
+ });
83
+ // The record leaves the process when a path is named. Registered *after* the token, so that every entry
84
+ // the file receives has already been through the redaction that knows about it — a sink is handed
85
+ // entries after `record` has redacted them, and that ordering is the reason this is a sink rather than a
86
+ // second writer with its own idea of what a secret looks like.
87
+ if (config.journalPath.length > 0) {
88
+ journal.onEntry(createJournalFileSink(config.journalPath));
89
+ }
67
90
  // The injection event is declared by `@deepseek-ai/dsh-host-webserver`, which this package deliberately
68
91
  // does not import: importing it would augment `Context` with a second declaration of `webServer` and
69
92
  // turn the structural dependency above into a compile error. The name is cast at this one boundary.
@@ -82,9 +105,14 @@ export function apply(ctx, config) {
82
105
  path: config.path,
83
106
  clients: () => clients.size,
84
107
  voice: () => ctx.serial('realtime-agent/status'),
85
- request: (verb) => verb === 'start'
86
- ? ctx.serial('realtime-agent/start')
87
- : ctx.serial('realtime-agent/stop'),
108
+ request: (verb) => {
109
+ // Only a start asks for a session; a stop is answered by the close entry that already follows it.
110
+ if (verb === 'start')
111
+ journal.record('session.requested', { trigger: 'strip' });
112
+ return verb === 'start'
113
+ ? ctx.serial('realtime-agent/start')
114
+ : ctx.serial('realtime-agent/stop');
115
+ },
88
116
  });
89
117
  /**
90
118
  * The tail of the control-reply queue.
@@ -167,8 +195,13 @@ export function apply(ctx, config) {
167
195
  journal.record('socket.accepted', { clients: String(clients.size), url: target });
168
196
  // Ask for the session before the bridge goes in, so the first frames are written into a session
169
197
  // that is being opened rather than dropped by the mic seam's no-session rule.
170
- if (live.openSessionOnConnect)
198
+ if (live.openSessionOnConnect) {
199
+ // Recorded on the side that asks. The opener records what came of the request, and the distance
200
+ // between the two entries is the only thing that separates a listener which never ran from a
201
+ // session nobody asked for — both of which are otherwise the same silence.
202
+ journal.record('session.requested', { trigger: 'connect', clients: String(clients.size) });
171
203
  ctx.emit('realtime-agent/start');
204
+ }
172
205
  attachAudioSocket(client, {
173
206
  emitMic: (pcm16) => { ctx.emit('realtime-agent/mic', pcm16); },
174
207
  subscribeAudio: (listener) => ctx.on('realtime-agent/audio', listener),
package/lib/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * A journal sink that appends to a file, so the record outlives the process that made it.
3
+ *
4
+ * The journal has always *held* everything worth knowing and has always been unable to hand it over: it is
5
+ * served over a route that wants a per-boot capability token, which means the record can be read by the page
6
+ * that holds the token and by nobody else, and it cannot be read after the fact at all. Every failure this
7
+ * plugin has had was diagnosed by reading somebody else's log — a harness session file off disk, or a
8
+ * `curl` at a route that answered 401. This is the missing half: the same entries, on disk, readable with
9
+ * `cat` by whoever is handed the file.
10
+ *
11
+ * ## Why `appendFileSync`, deliberately
12
+ *
13
+ * The ordering of a diagnostic record is the record. An asynchronous append can interleave two entries or
14
+ * lose one to a process that exits during the write, and the failure this exists to explain is usually the
15
+ * last thing that happened. So each line is written synchronously, as one line, and a reader can trust that
16
+ * the file is ordered and complete up to the last entry it contains.
17
+ *
18
+ * ## Redaction already happened
19
+ *
20
+ * A sink is handed entries *after* `Journal.record` has redacted them, so the file carries exactly what the
21
+ * route would have served and never the values named in `redactSecrets`. That ordering is the whole reason
22
+ * this is a sink and not a second writer with its own copy of the redaction rules — two code paths that must
23
+ * agree about what a secret looks like are two code paths that eventually disagree.
24
+ *
25
+ * @module dsh-realtime-audio-ws/journal-file
26
+ */
27
+ import type { JournalEntry } from 'dsh-realtime';
28
+ /**
29
+ * Build a sink that appends each entry to `path` as one JSON object per line.
30
+ *
31
+ * The parent directory is created here, at construction, rather than on the first write. That is the one
32
+ * failure this can have that is worth being loud about — a path that cannot be written — and at construction
33
+ * it is a startup error a caller can refuse to launch on, instead of a silent nothing that looks exactly
34
+ * like a quiet session. A *later* write that fails is swallowed by `Journal.onEntry`'s guard, by design.
35
+ * @param path - absolute or relative file path; the file is appended to, never truncated.
36
+ * @returns the sink, ready for `Journal.onEntry`.
37
+ */
38
+ export declare function createJournalFileSink(path: string): (entry: JournalEntry) => void;
39
+ //# sourceMappingURL=journal-file.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"journal-file.d.ts","sourceRoot":"","sources":["../src/journal-file.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAIH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAEhD;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAKjF"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * A journal sink that appends to a file, so the record outlives the process that made it.
3
+ *
4
+ * The journal has always *held* everything worth knowing and has always been unable to hand it over: it is
5
+ * served over a route that wants a per-boot capability token, which means the record can be read by the page
6
+ * that holds the token and by nobody else, and it cannot be read after the fact at all. Every failure this
7
+ * plugin has had was diagnosed by reading somebody else's log — a harness session file off disk, or a
8
+ * `curl` at a route that answered 401. This is the missing half: the same entries, on disk, readable with
9
+ * `cat` by whoever is handed the file.
10
+ *
11
+ * ## Why `appendFileSync`, deliberately
12
+ *
13
+ * The ordering of a diagnostic record is the record. An asynchronous append can interleave two entries or
14
+ * lose one to a process that exits during the write, and the failure this exists to explain is usually the
15
+ * last thing that happened. So each line is written synchronously, as one line, and a reader can trust that
16
+ * the file is ordered and complete up to the last entry it contains.
17
+ *
18
+ * ## Redaction already happened
19
+ *
20
+ * A sink is handed entries *after* `Journal.record` has redacted them, so the file carries exactly what the
21
+ * route would have served and never the values named in `redactSecrets`. That ordering is the whole reason
22
+ * this is a sink and not a second writer with its own copy of the redaction rules — two code paths that must
23
+ * agree about what a secret looks like are two code paths that eventually disagree.
24
+ *
25
+ * @module dsh-realtime-audio-ws/journal-file
26
+ */
27
+ import { appendFileSync, mkdirSync } from 'node:fs';
28
+ import { dirname } from 'node:path';
29
+ /**
30
+ * Build a sink that appends each entry to `path` as one JSON object per line.
31
+ *
32
+ * The parent directory is created here, at construction, rather than on the first write. That is the one
33
+ * failure this can have that is worth being loud about — a path that cannot be written — and at construction
34
+ * it is a startup error a caller can refuse to launch on, instead of a silent nothing that looks exactly
35
+ * like a quiet session. A *later* write that fails is swallowed by `Journal.onEntry`'s guard, by design.
36
+ * @param path - absolute or relative file path; the file is appended to, never truncated.
37
+ * @returns the sink, ready for `Journal.onEntry`.
38
+ */
39
+ export function createJournalFileSink(path) {
40
+ mkdirSync(dirname(path), { recursive: true });
41
+ return (entry) => {
42
+ appendFileSync(path, `${JSON.stringify(entry)}\n`);
43
+ };
44
+ }
45
+ //# sourceMappingURL=journal-file.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"journal-file.js","sourceRoot":"","sources":["../src/journal-file.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AACnD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAGnC;;;;;;;;;GASG;AACH,MAAM,UAAU,qBAAqB,CAAC,IAAY;IAChD,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IAC7C,OAAO,CAAC,KAAmB,EAAQ,EAAE;QACnC,cAAc,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;IACpD,CAAC,CAAA;AACH,CAAC"}
package/lib/strip.d.ts CHANGED
@@ -33,10 +33,17 @@ export declare const STRIP_ELEMENT_ID = "dsh-realtime-strip";
33
33
  /**
34
34
  * The panel's container, as markup.
35
35
  *
36
- * Fixed to the bottom right and deliberately nearly unstyled: it sits inside the host's own page, so it
37
- * inherits that page's font and colour rather than inventing a second visual language beside it. A
38
- * translucent wash rather than a border, because a border here would be visual noise around a strip whose
39
- * job is to be glanced at.
36
+ * Fixed to the bottom right, inheriting the host's font and colour rather than inventing a second visual
37
+ * language beside it — but **opaque**, and that is a correction rather than a taste. This panel sits over
38
+ * the host's own text, so a translucent wash lets the page show through and the two become unreadable
39
+ * together: two texts superimposed, neither of them legible. That was reported from a screenshot of the
40
+ * running app, which is the only way it was ever going to be found — nothing in a DOM test can see what is
41
+ * *behind* the panel.
42
+ *
43
+ * `Canvas` is the theme's own surface colour, so the panel follows light or dark mode without naming a
44
+ * palette of its own, and the 6% tint of the inherited colour keeps the surface from reading as a hole
45
+ * punched in the page. The soft shadow says it sits above the page — not a border, which would be visual
46
+ * noise around a strip whose job is to be glanced at.
40
47
  * @returns one `html` row's payload.
41
48
  */
42
49
  export declare function stripMarkup(): string;
@@ -1 +1 @@
1
- {"version":3,"file":"strip.d.ts","sourceRoot":"","sources":["../src/strip.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAEjD,8FAA8F;AAC9F,eAAO,MAAM,gBAAgB,uBAAuB,CAAA;AAEpD;;;;;;;;GAQG;AACH,wBAAgB,WAAW,IAAI,MAAM,CASpC;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,IAAI,MAAM,CAQvC;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,SAAS,IAAI,WAAW,EAAE,CAKzC"}
1
+ {"version":3,"file":"strip.d.ts","sourceRoot":"","sources":["../src/strip.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAEjD,8FAA8F;AAC9F,eAAO,MAAM,gBAAgB,uBAAuB,CAAA;AAEpD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,WAAW,IAAI,MAAM,CAUpC;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,IAAI,MAAM,CAQvC;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,SAAS,IAAI,WAAW,EAAE,CAKzC"}
package/lib/strip.js CHANGED
@@ -32,10 +32,17 @@ export const STRIP_ELEMENT_ID = 'dsh-realtime-strip';
32
32
  /**
33
33
  * The panel's container, as markup.
34
34
  *
35
- * Fixed to the bottom right and deliberately nearly unstyled: it sits inside the host's own page, so it
36
- * inherits that page's font and colour rather than inventing a second visual language beside it. A
37
- * translucent wash rather than a border, because a border here would be visual noise around a strip whose
38
- * job is to be glanced at.
35
+ * Fixed to the bottom right, inheriting the host's font and colour rather than inventing a second visual
36
+ * language beside it — but **opaque**, and that is a correction rather than a taste. This panel sits over
37
+ * the host's own text, so a translucent wash lets the page show through and the two become unreadable
38
+ * together: two texts superimposed, neither of them legible. That was reported from a screenshot of the
39
+ * running app, which is the only way it was ever going to be found — nothing in a DOM test can see what is
40
+ * *behind* the panel.
41
+ *
42
+ * `Canvas` is the theme's own surface colour, so the panel follows light or dark mode without naming a
43
+ * palette of its own, and the 6% tint of the inherited colour keeps the surface from reading as a hole
44
+ * punched in the page. The soft shadow says it sits above the page — not a border, which would be visual
45
+ * noise around a strip whose job is to be glanced at.
39
46
  * @returns one `html` row's payload.
40
47
  */
41
48
  export function stripMarkup() {
@@ -44,7 +51,8 @@ export function stripMarkup() {
44
51
  ' style="position:fixed;right:12px;bottom:12px;z-index:40;',
45
52
  'max-width:26rem;max-height:60vh;overflow:auto;padding:10px 12px;',
46
53
  'font:inherit;color:inherit;line-height:1.4;',
47
- 'background:color-mix(in srgb, currentColor 8%, transparent);border-radius:10px">',
54
+ 'background:color-mix(in srgb, Canvas 94%, currentColor);',
55
+ 'box-shadow:0 4px 16px rgb(0 0 0 / 0.18);border-radius:10px">',
48
56
  '</div>',
49
57
  ].join('');
50
58
  }
package/lib/strip.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"strip.js","sourceRoot":"","sources":["../src/strip.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAIH,8FAA8F;AAC9F,MAAM,CAAC,MAAM,gBAAgB,GAAG,oBAAoB,CAAA;AAEpD;;;;;;;;GAQG;AACH,MAAM,UAAU,WAAW;IACzB,OAAO;QACL,YAAY,gBAAgB,GAAG;QAC/B,2DAA2D;QAC3D,kEAAkE;QAClE,6CAA6C;QAC7C,kFAAkF;QAClF,QAAQ;KACT,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;AACZ,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc;IAC5B,OAAO;QACL,iBAAiB;QACjB,iHAAiH;QACjH,WAAW;QACX,kIAAkI;QAClI,MAAM;KACP,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AACd,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,SAAS;IACvB,OAAO;QACL,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE;QACxD,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc,EAAE,EAAE;KAC9D,CAAA;AACH,CAAC"}
1
+ {"version":3,"file":"strip.js","sourceRoot":"","sources":["../src/strip.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAIH,8FAA8F;AAC9F,MAAM,CAAC,MAAM,gBAAgB,GAAG,oBAAoB,CAAA;AAEpD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,WAAW;IACzB,OAAO;QACL,YAAY,gBAAgB,GAAG;QAC/B,2DAA2D;QAC3D,kEAAkE;QAClE,6CAA6C;QAC7C,0DAA0D;QAC1D,8DAA8D;QAC9D,QAAQ;KACT,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;AACZ,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc;IAC5B,OAAO;QACL,iBAAiB;QACjB,iHAAiH;QACjH,WAAW;QACX,kIAAkI;QAClI,MAAM;KACP,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AACd,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,SAAS;IACvB,OAAO;QACL,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE;QACxD,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc,EAAE,EAAE;KAC9D,CAAA;AACH,CAAC"}
package/lib/types.d.ts CHANGED
@@ -61,6 +61,14 @@ export interface RealtimeAudioWsConfig {
61
61
  * more to a stranger than an empty socket.
62
62
  */
63
63
  readonly diagnosticsPath: string;
64
+ /**
65
+ * File the journal is appended to, one JSON object per line. Empty means no file.
66
+ *
67
+ * Off by default, and named by the deployment that wants a recording: the journal is held in memory and
68
+ * served over a route that wants a capability token, so a deployment that needs the record to outlive the
69
+ * process — or to be handed to somebody else — says so here.
70
+ */
71
+ readonly journalPath: string;
64
72
  }
65
73
  /**
66
74
  * The slice of a WebSocket this package uses, described structurally.
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,0GAA0G;AAC1G,eAAO,MAAM,YAAY,wBAAwB,CAAA;AAEjD,6FAA6F;AAC7F,eAAO,MAAM,uBAAuB,SAAU,CAAA;AAE9C,sFAAsF;AACtF,eAAO,MAAM,uBAAuB,IAAI,CAAA;AAExC,0GAA0G;AAC1G,eAAO,MAAM,+BAA+B,OAAO,CAAA;AAEnD;;;;;;GAMG;AACH,eAAO,MAAM,wBAAwB,8BAA8B,CAAA;AAEnE,MAAM,WAAW,qBAAqB;IACpC;;;OAGG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB;;;OAGG;IACH,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;IAC9B;;;OAGG;IACH,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAA;IAC/B;;;;;;;;OAQG;IACH,QAAQ,CAAC,oBAAoB,EAAE,OAAO,CAAA;IACtC;;;;;;OAMG;IACH,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAA;CACjC;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,GAAG,IAAI,CAAA;IACrC,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3C,SAAS,IAAI,IAAI,CAAA;IACjB,EAAE,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,KAAK,IAAI,GAAG,OAAO,CAAA;IACnF,EAAE,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,GAAG,OAAO,CAAA;CAC9E"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,0GAA0G;AAC1G,eAAO,MAAM,YAAY,wBAAwB,CAAA;AAEjD,6FAA6F;AAC7F,eAAO,MAAM,uBAAuB,SAAU,CAAA;AAE9C,sFAAsF;AACtF,eAAO,MAAM,uBAAuB,IAAI,CAAA;AAExC,0GAA0G;AAC1G,eAAO,MAAM,+BAA+B,OAAO,CAAA;AAEnD;;;;;;GAMG;AACH,eAAO,MAAM,wBAAwB,8BAA8B,CAAA;AAEnE,MAAM,WAAW,qBAAqB;IACpC;;;OAGG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB;;;OAGG;IACH,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;IAC9B;;;OAGG;IACH,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAA;IAC/B;;;;;;;;OAQG;IACH,QAAQ,CAAC,oBAAoB,EAAE,OAAO,CAAA;IACtC;;;;;;OAMG;IACH,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAA;IAChC;;;;;;OAMG;IACH,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;CAC7B;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,GAAG,IAAI,CAAA;IACrC,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3C,SAAS,IAAI,IAAI,CAAA;IACjB,EAAE,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,KAAK,IAAI,GAAG,OAAO,CAAA;IACnF,EAAE,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,GAAG,OAAO,CAAA;CAC9E"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-realtime-audio-ws",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "The host end of the client half's transport: a WebSocket upgrade route that bridges microphone audio in and agent speech out.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -52,8 +52,8 @@
52
52
  },
53
53
  "dependencies": {
54
54
  "ws": "^8.22.0",
55
- "dsh-realtime": "^0.2.3",
56
- "dsh-realtime-agent": "^0.2.7"
55
+ "dsh-realtime": "^0.2.6",
56
+ "dsh-realtime-agent": "^0.2.9"
57
57
  },
58
58
  "peerDependencies": {
59
59
  "@deepseek-ai/cordis": "^4.0.3",
@@ -359,6 +359,12 @@ export function createAudioClient(deps: ClientAudioDeps): ClientAudio {
359
359
  const source = context.createMediaStreamSource(stream)
360
360
  const node = context.createScriptProcessor(CAPTURE_BUFFER, 1, 1)
361
361
  node.onaudioprocess = (event) => {
362
+ // Every quantum ships, **silent ones included, and that is load-bearing rather than incidental**: the
363
+ // provider's session timeline advances with the audio this client sends, and a context append is
364
+ // placed only while that timeline is advancing — so a client that skipped silent blocks (an
365
+ // obvious-looking saving) would silently freeze the session and lose every append after it. Measured,
366
+ // with the mechanism, in `docs/usable-window.md`; guarded by the capture-continuity case in
367
+ // `tests/client.spec.ts`.
362
368
  const pcm16 = pcm16FromFloat32(event.inputBuffer.getChannelData(0))
363
369
  socket?.send(new Uint8Array(pcm16.buffer))
364
370
  }
package/src/index.ts CHANGED
@@ -72,6 +72,8 @@ export { diagnosticsRoute, type DiagnosticsDeps, type DiagnosticsJournal, type D
72
72
  export { STRIP_ELEMENT_ID, stripBootstrap, stripMarkup, stripRows } from './strip.ts'
73
73
  export type { InjectedGlobalRow, InjectedRow } from './injection.ts'
74
74
 
75
+ import { createJournalFileSink } from './journal-file.ts'
76
+
75
77
  /** Plugin name. */
76
78
  export const name = 'realtime-audio-ws'
77
79
 
@@ -84,6 +86,10 @@ export const Config = Schema.object({
84
86
  // microphone: a connection takes an explicit action, an authenticated one, and its absence is silence.
85
87
  openSessionOnConnect: Schema.boolean().default(DEFAULT_OPEN_SESSION_ON_CONNECT),
86
88
  diagnosticsPath: Schema.string().default(DEFAULT_DIAGNOSTICS_PATH),
89
+ // Empty means no file, and empty is the default: a plugin that writes files because it was installed is a
90
+ // plugin writing files in somebody else's directory. The deployment that wants a recording is the
91
+ // deployment that names a path.
92
+ journalPath: Schema.string().default(''),
87
93
  })
88
94
 
89
95
  /**
@@ -137,6 +143,24 @@ export function apply(ctx: Context, config: RealtimeAudioWsConfig): void {
137
143
  // constructor argument: the plugin that mints the token is the only one that can name it, and it
138
144
  // mints it here, after the journal already exists.
139
145
  journal.addSecrets([token])
146
+ // What this plugin resolved, so the journal can be read without the profile beside it. Values only, and
147
+ // no field that could carry a secret: `journalPath` names a file, and the token went in on the line above
148
+ // so that it can never reach a record in the first place.
149
+ journal.record('config.resolved', {
150
+ plugin: 'dsh-realtime-audio-ws',
151
+ path: config.path,
152
+ openSessionOnConnect: String(config.openSessionOnConnect),
153
+ maxConnections: String(config.maxConnections),
154
+ maxFrameBytes: String(config.maxFrameBytes),
155
+ journalPath: config.journalPath,
156
+ })
157
+ // The record leaves the process when a path is named. Registered *after* the token, so that every entry
158
+ // the file receives has already been through the redaction that knows about it — a sink is handed
159
+ // entries after `record` has redacted them, and that ordering is the reason this is a sink rather than a
160
+ // second writer with its own idea of what a secret looks like.
161
+ if (config.journalPath.length > 0) {
162
+ journal.onEntry(createJournalFileSink(config.journalPath))
163
+ }
140
164
  // The injection event is declared by `@deepseek-ai/dsh-host-webserver`, which this package deliberately
141
165
  // does not import: importing it would augment `Context` with a second declaration of `webServer` and
142
166
  // turn the structural dependency above into a compile error. The name is cast at this one boundary.
@@ -159,9 +183,13 @@ export function apply(ctx: Context, config: RealtimeAudioWsConfig): void {
159
183
  path: config.path,
160
184
  clients: () => clients.size,
161
185
  voice: () => ctx.serial('realtime-agent/status'),
162
- request: (verb) => verb === 'start'
163
- ? ctx.serial('realtime-agent/start')
164
- : ctx.serial('realtime-agent/stop'),
186
+ request: (verb) => {
187
+ // Only a start asks for a session; a stop is answered by the close entry that already follows it.
188
+ if (verb === 'start') journal.record('session.requested', { trigger: 'strip' })
189
+ return verb === 'start'
190
+ ? ctx.serial('realtime-agent/start')
191
+ : ctx.serial('realtime-agent/stop')
192
+ },
165
193
  })
166
194
 
167
195
  /**
@@ -255,7 +283,13 @@ export function apply(ctx: Context, config: RealtimeAudioWsConfig): void {
255
283
  journal.record('socket.accepted', { clients: String(clients.size), url: target })
256
284
  // Ask for the session before the bridge goes in, so the first frames are written into a session
257
285
  // that is being opened rather than dropped by the mic seam's no-session rule.
258
- if (live.openSessionOnConnect) ctx.emit('realtime-agent/start')
286
+ if (live.openSessionOnConnect) {
287
+ // Recorded on the side that asks. The opener records what came of the request, and the distance
288
+ // between the two entries is the only thing that separates a listener which never ran from a
289
+ // session nobody asked for — both of which are otherwise the same silence.
290
+ journal.record('session.requested', { trigger: 'connect', clients: String(clients.size) })
291
+ ctx.emit('realtime-agent/start')
292
+ }
259
293
  attachAudioSocket(client, {
260
294
  emitMic: (pcm16) => { ctx.emit('realtime-agent/mic', pcm16) },
261
295
  subscribeAudio: (listener) => ctx.on('realtime-agent/audio', listener),
@@ -0,0 +1,47 @@
1
+ /**
2
+ * A journal sink that appends to a file, so the record outlives the process that made it.
3
+ *
4
+ * The journal has always *held* everything worth knowing and has always been unable to hand it over: it is
5
+ * served over a route that wants a per-boot capability token, which means the record can be read by the page
6
+ * that holds the token and by nobody else, and it cannot be read after the fact at all. Every failure this
7
+ * plugin has had was diagnosed by reading somebody else's log — a harness session file off disk, or a
8
+ * `curl` at a route that answered 401. This is the missing half: the same entries, on disk, readable with
9
+ * `cat` by whoever is handed the file.
10
+ *
11
+ * ## Why `appendFileSync`, deliberately
12
+ *
13
+ * The ordering of a diagnostic record is the record. An asynchronous append can interleave two entries or
14
+ * lose one to a process that exits during the write, and the failure this exists to explain is usually the
15
+ * last thing that happened. So each line is written synchronously, as one line, and a reader can trust that
16
+ * the file is ordered and complete up to the last entry it contains.
17
+ *
18
+ * ## Redaction already happened
19
+ *
20
+ * A sink is handed entries *after* `Journal.record` has redacted them, so the file carries exactly what the
21
+ * route would have served and never the values named in `redactSecrets`. That ordering is the whole reason
22
+ * this is a sink and not a second writer with its own copy of the redaction rules — two code paths that must
23
+ * agree about what a secret looks like are two code paths that eventually disagree.
24
+ *
25
+ * @module dsh-realtime-audio-ws/journal-file
26
+ */
27
+
28
+ import { appendFileSync, mkdirSync } from 'node:fs'
29
+ import { dirname } from 'node:path'
30
+ import type { JournalEntry } from 'dsh-realtime'
31
+
32
+ /**
33
+ * Build a sink that appends each entry to `path` as one JSON object per line.
34
+ *
35
+ * The parent directory is created here, at construction, rather than on the first write. That is the one
36
+ * failure this can have that is worth being loud about — a path that cannot be written — and at construction
37
+ * it is a startup error a caller can refuse to launch on, instead of a silent nothing that looks exactly
38
+ * like a quiet session. A *later* write that fails is swallowed by `Journal.onEntry`'s guard, by design.
39
+ * @param path - absolute or relative file path; the file is appended to, never truncated.
40
+ * @returns the sink, ready for `Journal.onEntry`.
41
+ */
42
+ export function createJournalFileSink(path: string): (entry: JournalEntry) => void {
43
+ mkdirSync(dirname(path), { recursive: true })
44
+ return (entry: JournalEntry): void => {
45
+ appendFileSync(path, `${JSON.stringify(entry)}\n`)
46
+ }
47
+ }
package/src/strip.ts CHANGED
@@ -36,10 +36,17 @@ export const STRIP_ELEMENT_ID = 'dsh-realtime-strip'
36
36
  /**
37
37
  * The panel's container, as markup.
38
38
  *
39
- * Fixed to the bottom right and deliberately nearly unstyled: it sits inside the host's own page, so it
40
- * inherits that page's font and colour rather than inventing a second visual language beside it. A
41
- * translucent wash rather than a border, because a border here would be visual noise around a strip whose
42
- * job is to be glanced at.
39
+ * Fixed to the bottom right, inheriting the host's font and colour rather than inventing a second visual
40
+ * language beside it — but **opaque**, and that is a correction rather than a taste. This panel sits over
41
+ * the host's own text, so a translucent wash lets the page show through and the two become unreadable
42
+ * together: two texts superimposed, neither of them legible. That was reported from a screenshot of the
43
+ * running app, which is the only way it was ever going to be found — nothing in a DOM test can see what is
44
+ * *behind* the panel.
45
+ *
46
+ * `Canvas` is the theme's own surface colour, so the panel follows light or dark mode without naming a
47
+ * palette of its own, and the 6% tint of the inherited colour keeps the surface from reading as a hole
48
+ * punched in the page. The soft shadow says it sits above the page — not a border, which would be visual
49
+ * noise around a strip whose job is to be glanced at.
43
50
  * @returns one `html` row's payload.
44
51
  */
45
52
  export function stripMarkup(): string {
@@ -48,7 +55,8 @@ export function stripMarkup(): string {
48
55
  ' style="position:fixed;right:12px;bottom:12px;z-index:40;',
49
56
  'max-width:26rem;max-height:60vh;overflow:auto;padding:10px 12px;',
50
57
  'font:inherit;color:inherit;line-height:1.4;',
51
- 'background:color-mix(in srgb, currentColor 8%, transparent);border-radius:10px">',
58
+ 'background:color-mix(in srgb, Canvas 94%, currentColor);',
59
+ 'box-shadow:0 4px 16px rgb(0 0 0 / 0.18);border-radius:10px">',
52
60
  '</div>',
53
61
  ].join('')
54
62
  }
package/src/types.ts CHANGED
@@ -67,6 +67,14 @@ export interface RealtimeAudioWsConfig {
67
67
  * more to a stranger than an empty socket.
68
68
  */
69
69
  readonly diagnosticsPath: string
70
+ /**
71
+ * File the journal is appended to, one JSON object per line. Empty means no file.
72
+ *
73
+ * Off by default, and named by the deployment that wants a recording: the journal is held in memory and
74
+ * served over a route that wants a capability token, so a deployment that needs the record to outlive the
75
+ * process — or to be handed to somebody else — says so here.
76
+ */
77
+ readonly journalPath: string
70
78
  }
71
79
 
72
80
  /**