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.
- package/lib/client/bundle.js +6 -0
- package/lib/client/index.js +6 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +37 -4
- package/lib/index.js.map +1 -1
- package/lib/journal-file.d.ts +39 -0
- package/lib/journal-file.d.ts.map +1 -0
- package/lib/journal-file.js +45 -0
- package/lib/journal-file.js.map +1 -0
- package/lib/strip.d.ts +11 -4
- package/lib/strip.d.ts.map +1 -1
- package/lib/strip.js +13 -5
- package/lib/strip.js.map +1 -1
- package/lib/types.d.ts +8 -0
- package/lib/types.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/client/index.ts +6 -0
- package/src/index.ts +38 -4
- package/src/journal-file.ts +47 -0
- package/src/strip.ts +13 -5
- package/src/types.ts +8 -0
package/lib/client/bundle.js
CHANGED
|
@@ -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
|
};
|
package/lib/client/index.js
CHANGED
|
@@ -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
|
package/lib/index.d.ts.map
CHANGED
|
@@ -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;
|
|
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) =>
|
|
86
|
-
|
|
87
|
-
|
|
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":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,MAAM,MAAM,0BAA0B,CAAA;AAM7C,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAC/C,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAA;AACnD,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AACnD,OAAO,EACL,gBAAgB,EAChB,cAAc,EACd,iBAAiB,EACjB,YAAY,EACZ,UAAU,GAEX,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AACtC,OAAO,EAAE,qBAAqB,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AACnE,OAAO,EACL,wBAAwB,EACxB,uBAAuB,EACvB,uBAAuB,EACvB,+BAA+B,EAC/B,YAAY,GAGb,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,EAA8B,MAAM,aAAa,CAAA;AAC5F,OAAO,EACL,oBAAoB,EACpB,iBAAiB,GAUlB,MAAM,cAAc,CAAA;AACrB,OAAO,EAAE,qBAAqB,EAAE,aAAa,EAAwB,MAAM,cAAc,CAAA;AACzF,OAAO,EAAE,gBAAgB,EAAwE,MAAM,kBAAkB,CAAA;AACzH,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAGrF,mBAAmB;AACnB,MAAM,CAAC,MAAM,IAAI,GAAG,mBAAmB,CAAA;AAEvC,MAAM,CAAC,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;IAClC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,YAAY,CAAC;IAC3C,aAAa,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,uBAAuB,CAAC;IAChE,cAAc,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,uBAAuB,CAAC;IACjE,uGAAuG;IACvG,0GAA0G;IAC1G,uGAAuG;IACvG,oBAAoB,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,+BAA+B,CAAC;IAC/E,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,wBAAwB,CAAC;
|
|
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
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
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;
|
package/lib/strip.d.ts.map
CHANGED
|
@@ -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
|
|
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
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
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,
|
|
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
|
|
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.
|
package/lib/types.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
+
"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.
|
|
56
|
-
"dsh-realtime-agent": "^0.2.
|
|
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",
|
package/src/client/index.ts
CHANGED
|
@@ -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) =>
|
|
163
|
-
|
|
164
|
-
|
|
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)
|
|
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
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
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,
|
|
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
|
/**
|