@klarkxy/dsh-deep-view 0.1.0 → 0.2.0

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/index.js CHANGED
@@ -1,12 +1,666 @@
1
- import { RPC_CHANNEL, VIEW_OWNER, isRecord as isRecord$1 } from "./contracts.js";
1
+ import { _ as viewSummary, i as checkShownView, p as validateViewAnswer } from "./core2.js";
2
+ import { HOST_SERVICE, PLUGIN_NAME, RPC_CHANNEL, VIEW_OWNER, isRecord } from "./contracts.js";
3
+ import { answerText } from "./answer-text.js";
2
4
  import { randomUUID } from "node:crypto";
3
5
  import { createUserMessage } from "@deepseek-ai/dsh-llm";
6
+ import { AsyncLocalStorage } from "node:async_hooks";
7
+ import { appendFileSync, existsSync, lstatSync, mkdirSync, readdirSync, realpathSync, renameSync, unlinkSync } from "node:fs";
8
+ import { homedir } from "node:os";
9
+ import { basename, dirname, join, resolve, sep } from "node:path";
10
+ import { fileURLToPath } from "node:url";
11
+ //#region ../dsh-plugin-kit/src/log-shared.ts
12
+ const PLUGIN_LOG_LEVELS = [
13
+ "debug",
14
+ "info",
15
+ "warn",
16
+ "error",
17
+ "fault"
18
+ ];
19
+ const DEFAULT_PLUGIN_LOG_LEVEL = "info";
20
+ const PLUGIN_LOG_MAX_MESSAGE_CHARS = 16e3;
21
+ const SOURCE_RE = /^[a-z][a-z0-9-]{0,63}$/;
22
+ const EVENT_RE = /^[a-z][a-z0-9._-]{0,127}$/;
23
+ const CONTEXT_KEY_RE = /^[a-zA-Z][a-zA-Z0-9._-]{0,63}$/;
24
+ const SECRET_KEY_RE = /(?:api[_-]?key|access[_-]?token|refresh[_-]?token|bearer|authorization|cookie|set[_-]?cookie|nonce|credential|private[_-]?key|password|secret|app[_-]?secret|client[_-]?secret|z_c0|d_c0)$/i;
25
+ const CHAT_PAYLOAD_RE = /"role"\s*:\s*"(?:system|user|assistant|tool)"|"(?:messages|content|prompt|input_text|input_image|image_url)"\s*:\s*[\[{"']/i;
26
+ const CREDENTIAL_BODY_RE = /-----BEGIN (?:RSA |EC |OPENSSH |PRIVATE )?PRIVATE KEY-----|"type"\s*:\s*"service_account"|credentials\.json|\.dsh[/\\](?:credentials|auth)/i;
27
+ const AUTH_HEADER_RE = /\b(Authorization|Proxy-Authorization|Cookie|Set-Cookie)\s*:\s*[^\r\n]*/gi;
28
+ const BEARER_RE = /\bBearer\s+[A-Za-z0-9._\-+=/]{8,}/g;
29
+ const SECRET_FIELD_RE = /(["']?(?:api[_-]?key|access[_-]?token|refresh[_-]?token|token|bearer|authorization|cookie|set-cookie|nonce|credential|private[_-]?key|password|secret|app[_-]?secret|client[_-]?secret|password|z_c0|d_c0)["']?\s*[:=]\s*)(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[^\s,&;}]+)/gi;
30
+ const SECRET_QUERY_RE = /([?&](?:token|api[_-]?key|key|secret|password|bootstrap|code|sig|signature|credential|password)=)[^\s&#"']+/gi;
31
+ const URL_CREDENTIAL_RE = /\b(https?:\/\/)[^\s/:@]+:[^\s/@]+@/gi;
32
+ const BOOTSTRAP_RE = /\/bootstrap\/[^\s"'?&#]+/gi;
33
+ const KNOWN_TOKEN_RE = /\b(?:sk-[A-Za-z0-9_-]{10,}|gh[pousr]_[A-Za-z0-9_]{16,}|github_pat_[A-Za-z0-9_]{20,}|tvly-[A-Za-z0-9_-]{10,}|AKIA[0-9A-Z]{16})\b/g;
34
+ const PATH_RE = /(?:[A-Za-z]:\\|\\\\)[^\s"'`<>|]+|(?:\/(?:home|Users|tmp|var|root)\/|~\/)[^\s"'`<>|]+/g;
35
+ function isPluginLogLevel(value) {
36
+ return typeof value === "string" && PLUGIN_LOG_LEVELS.includes(value);
37
+ }
38
+ function pluginLogLevelEnabled(level, minimum) {
39
+ return PLUGIN_LOG_LEVELS.indexOf(level) >= PLUGIN_LOG_LEVELS.indexOf(minimum);
40
+ }
41
+ function normalizePluginLogSource(raw) {
42
+ const stripped = String(raw || "").trim().replace(/^@[^/]+\//, "").toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
43
+ const bounded = (/^[a-z]/.test(stripped) ? stripped : `p-${stripped}`).slice(0, 64).replace(/-+$/g, "");
44
+ return SOURCE_RE.test(bounded) ? bounded : "dsh-plugin";
45
+ }
46
+ function normalizePluginLogEvent(raw) {
47
+ const cleaned = String(raw || "").trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^[^a-z]+/, "").slice(0, 128);
48
+ return EVENT_RE.test(cleaned) ? cleaned : "plugin.event";
49
+ }
50
+ /**
51
+ * Parse a level specification like `debug` or `dsh-zhihu=debug,dsh-dot:warn,info`.
52
+ * Source-specific rules win over a bare fallback level.
53
+ */
54
+ function parsePluginLogLevel(spec, source, fallback = DEFAULT_PLUGIN_LOG_LEVEL) {
55
+ if (!spec || typeof spec !== "string") return fallback;
56
+ const normalizedSource = source ? normalizePluginLogSource(source) : void 0;
57
+ let wildcard;
58
+ for (const token of spec.split(",").map((part) => part.trim()).filter(Boolean)) {
59
+ const lowered = token.toLowerCase();
60
+ if (isPluginLogLevel(lowered)) {
61
+ wildcard = lowered;
62
+ continue;
63
+ }
64
+ const match = /^([^=:]+)[=:]([a-z]+)$/i.exec(token);
65
+ if (!match) continue;
66
+ const target = match[1].trim();
67
+ const candidate = match[2].trim().toLowerCase();
68
+ if (!isPluginLogLevel(candidate)) continue;
69
+ if (target === "*") {
70
+ wildcard = candidate;
71
+ continue;
72
+ }
73
+ if (normalizedSource && normalizePluginLogSource(target) === normalizedSource) return candidate;
74
+ }
75
+ return wildcard ?? fallback;
76
+ }
77
+ function sanitizePluginLogText(text, maxChars = PLUGIN_LOG_MAX_MESSAGE_CHARS) {
78
+ if (!text) return "";
79
+ if (CREDENTIAL_BODY_RE.test(text)) return "[omitted credential material]";
80
+ if (CHAT_PAYLOAD_RE.test(text)) return "[omitted chat payload]";
81
+ return truncateLogText(text.replace(AUTH_HEADER_RE, "$1: [redacted]").replace(BEARER_RE, "Bearer [redacted]").replace(SECRET_FIELD_RE, (full, prefix) => {
82
+ const value = full.slice(prefix.length);
83
+ if (value.startsWith("\"")) return `${prefix}"[redacted]"`;
84
+ if (value.startsWith("'")) return `${prefix}'[redacted]'`;
85
+ return `${prefix}[redacted]`;
86
+ }).replace(SECRET_QUERY_RE, "$1[redacted]").replace(URL_CREDENTIAL_RE, "$1[redacted]@").replace(BOOTSTRAP_RE, "/bootstrap/[redacted]").replace(KNOWN_TOKEN_RE, "[redacted]").replace(PATH_RE, "[path]"), maxChars);
87
+ }
88
+ function sanitizePluginLogContext(input) {
89
+ if (!input) return void 0;
90
+ const output = {};
91
+ for (const [key, value] of Object.entries(input).slice(0, 32)) {
92
+ if (value === void 0 || !CONTEXT_KEY_RE.test(key)) continue;
93
+ if (typeof value === "number" && !Number.isFinite(value)) continue;
94
+ if (SECRET_KEY_RE.test(key)) {
95
+ output[key] = "[redacted]";
96
+ continue;
97
+ }
98
+ output[key] = typeof value === "string" ? sanitizePluginLogText(value, 1e3) : value;
99
+ }
100
+ if (!Object.keys(output).length) return void 0;
101
+ if (JSON.stringify(output).length <= 8e3) return output;
102
+ return {
103
+ omitted: true,
104
+ reason: "Diagnostic context exceeded its size limit."
105
+ };
106
+ }
107
+ function sanitizePluginLogError(error) {
108
+ if (error === void 0) return void 0;
109
+ const row = error && typeof error === "object" ? error : {};
110
+ const rawMessage = typeof row.message === "string" ? row.message : String(error);
111
+ const result = {
112
+ name: typeof row.name === "string" && row.name.trim() ? truncateLogText(row.name.trim(), 100) : "Error",
113
+ message: sanitizePluginLogText(rawMessage) || "Unknown error"
114
+ };
115
+ if (typeof row.stack === "string" && row.stack.trim()) result.stack = sanitizePluginLogText(row.stack, 24e3);
116
+ if (typeof row.code === "string" && /^[A-Za-z0-9_./:-]{1,100}$/.test(row.code)) result.code = row.code;
117
+ else if (typeof row.code === "number" && Number.isFinite(row.code)) result.code = String(row.code);
118
+ return result;
119
+ }
120
+ function mergePluginLogContext(base, extra) {
121
+ if (!base) return extra;
122
+ if (!extra) return base;
123
+ return {
124
+ ...base,
125
+ ...extra
126
+ };
127
+ }
128
+ function buildPluginLogEntry(input, meta) {
129
+ const entry = {
130
+ version: 1,
131
+ at: meta.at.toISOString(),
132
+ level: isPluginLogLevel(input.level) ? input.level : "info",
133
+ source: normalizePluginLogSource(meta.source),
134
+ surface: meta.surface,
135
+ ...meta.profile ? { profile: meta.profile } : {},
136
+ event: normalizePluginLogEvent(input.event),
137
+ message: sanitizePluginLogText(input.message || "No diagnostic message was provided.", PLUGIN_LOG_MAX_MESSAGE_CHARS),
138
+ ...meta.pid !== void 0 ? { pid: meta.pid } : {}
139
+ };
140
+ const context = sanitizePluginLogContext(mergePluginLogContext(meta.correlation, input.context));
141
+ if (context) entry.context = context;
142
+ const error = sanitizePluginLogError(input.error);
143
+ if (error) entry.error = error;
144
+ return entry;
145
+ }
146
+ function createChildPluginLogger(parent, bound) {
147
+ return {
148
+ get source() {
149
+ return parent.source;
150
+ },
151
+ get file() {
152
+ return parent.file;
153
+ },
154
+ get lastWriteError() {
155
+ return parent.lastWriteError;
156
+ },
157
+ debug: (event, message, context) => parent.debug(event, message, mergePluginLogContext(bound, context)),
158
+ info: (event, message, context) => parent.info(event, message, mergePluginLogContext(bound, context)),
159
+ warn: (event, message, error, context) => parent.warn(event, message, error, mergePluginLogContext(bound, context)),
160
+ error: (event, message, error, context) => parent.error(event, message, error, mergePluginLogContext(bound, context)),
161
+ fault: (event, message, error, context) => parent.fault(event, message, error, mergePluginLogContext(bound, context)),
162
+ write: (input) => parent.write({
163
+ ...input,
164
+ context: mergePluginLogContext(bound, input.context)
165
+ }),
166
+ child: (next) => createChildPluginLogger(parent, mergePluginLogContext(bound, next) ?? {})
167
+ };
168
+ }
169
+ function truncateLogText(text, maxChars) {
170
+ if (text.length <= maxChars) return text;
171
+ const marker = "[truncated]\n";
172
+ if (maxChars <= 12) return text.slice(0, Math.max(0, maxChars));
173
+ return `${marker}${text.slice(-(maxChars - 12))}`;
174
+ }
175
+ const PROFILE_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/;
176
+ const ARCHIVE_RE = /^[a-z][a-z0-9-]{0,63}\.\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}\.\d{3}Z\.\d+\.\d+\.jsonl$/;
177
+ const correlationStorage = new AsyncLocalStorage();
178
+ const processFileLoggers = /* @__PURE__ */ new Map();
179
+ let operationSequence = 0;
180
+ function safeProfileName(profile) {
181
+ const trimmed = String(profile ?? "").trim();
182
+ if (PROFILE_RE.test(trimmed)) return trimmed.toLowerCase();
183
+ const cleaned = trimmed.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
184
+ return cleaned && PROFILE_RE.test(cleaned) ? cleaned : "default";
185
+ }
186
+ function pluginLogDirectory(home, profile = "default") {
187
+ return join(resolve(home), "logs", "plugins", safeProfileName(profile));
188
+ }
189
+ function resolveHostLogTarget(ctx, options = {}) {
190
+ const env = options.env ?? process.env;
191
+ if (options.directory?.trim()) {
192
+ const dir = resolve(options.directory.trim());
193
+ return {
194
+ directory: dir,
195
+ anchor: dir,
196
+ ...options.profile ? { profile: safeProfileName(options.profile) } : {}
197
+ };
198
+ }
199
+ if (options.home?.trim()) {
200
+ const home = resolve(options.home.trim());
201
+ const profile = safeProfileName(options.profile);
202
+ return {
203
+ directory: pluginLogDirectory(home, profile),
204
+ anchor: home,
205
+ profile
206
+ };
207
+ }
208
+ if (env.DSH_PLUGIN_LOG_DIR?.trim()) {
209
+ const dir = resolve(env.DSH_PLUGIN_LOG_DIR.trim());
210
+ return {
211
+ directory: dir,
212
+ anchor: dir,
213
+ ...options.profile ? { profile: safeProfileName(options.profile) } : {}
214
+ };
215
+ }
216
+ const host = ctx && typeof ctx === "object" ? ctx : void 0;
217
+ let profileCtx;
218
+ try {
219
+ profileCtx = host?.get?.("profileContext", false);
220
+ } catch {
221
+ try {
222
+ profileCtx = host?.get?.("profileContext");
223
+ } catch {
224
+ profileCtx = void 0;
225
+ }
226
+ }
227
+ if (typeof profileCtx?.home === "string" && profileCtx.home.trim() && typeof profileCtx.dir === "string" && profileCtx.dir.trim()) {
228
+ const home = resolve(profileCtx.home.trim());
229
+ const dir = resolve(profileCtx.dir.trim());
230
+ if (isInside(home, dir)) {
231
+ const profile = safeProfileName(typeof profileCtx.name === "string" ? profileCtx.name : basename(dir));
232
+ return {
233
+ directory: pluginLogDirectory(home, profile),
234
+ anchor: home,
235
+ profile
236
+ };
237
+ }
238
+ }
239
+ if (!Boolean(env.VITEST || env.NODE_TEST_CONTEXT) && typeof host?.baseUrl === "string" && host.baseUrl.trim()) {
240
+ const basePath = pathFromBaseUrl(host.baseUrl);
241
+ const configuredHome = resolve((env.DSH_HOME?.trim() || join(homedir(), ".dsh")).trim());
242
+ const profilesRoot = join(configuredHome, "profiles");
243
+ if (basePath && isInside(profilesRoot, basePath) && !samePath(profilesRoot, basePath)) {
244
+ const firstSegment = basePath.slice(profilesRoot.length).replace(/^[/\\]+/, "").split(/[/\\]/)[0];
245
+ if (firstSegment) {
246
+ const profile = safeProfileName(firstSegment);
247
+ return {
248
+ directory: pluginLogDirectory(configuredHome, profile),
249
+ anchor: configuredHome,
250
+ profile
251
+ };
252
+ }
253
+ }
254
+ }
255
+ return {
256
+ directory: null,
257
+ anchor: null,
258
+ ...options.profile ? { profile: safeProfileName(options.profile) } : {}
259
+ };
260
+ }
261
+ /**
262
+ * Synchronous, append-only rotating JSONL logger modeled on DSH Spaces
263
+ * diagnostics: 7-day or 100 MiB active rotation, 7-day / 100 MiB archive
264
+ * retention, secret and path redaction, and non-throwing write resilience.
265
+ */
266
+ var RotatingPluginLogger = class {
267
+ source;
268
+ file;
269
+ lastError = null;
270
+ size = 0;
271
+ openedAt = 0;
272
+ initialized = false;
273
+ writing = false;
274
+ sequence = 0;
275
+ loggedWriteFailure = false;
276
+ recent = [];
277
+ directory;
278
+ anchor;
279
+ profile;
280
+ minimum;
281
+ now;
282
+ pid;
283
+ maxFileBytes;
284
+ maxArchiveBytes;
285
+ maxAgeMs;
286
+ recentLimit;
287
+ constructor(source, options = {}) {
288
+ this.source = normalizePluginLogSource(source);
289
+ const env = options.env ?? process.env;
290
+ const resolved = options.directory !== void 0 || options.home !== void 0 ? resolveHostLogTarget(void 0, options) : {
291
+ directory: null,
292
+ anchor: options.anchor ?? null,
293
+ profile: options.profile
294
+ };
295
+ this.directory = resolved.directory;
296
+ this.anchor = options.anchor ?? resolved.anchor;
297
+ this.profile = options.profile ?? resolved.profile;
298
+ this.file = this.directory ? join(this.directory, `${this.source}.jsonl`) : null;
299
+ this.minimum = options.minimumLevel ?? parsePluginLogLevel(env.DSH_PLUGIN_LOG_LEVEL, this.source, "info");
300
+ this.now = options.now ?? (() => /* @__PURE__ */ new Date());
301
+ this.pid = options.pid ?? process.pid;
302
+ this.maxFileBytes = options.maxFileBytes ?? 104857600;
303
+ this.maxArchiveBytes = options.maxArchiveBytes ?? 104857600;
304
+ this.maxAgeMs = options.maxAgeMs ?? 6048e5;
305
+ this.recentLimit = Math.max(1, options.recentLimit ?? 200);
306
+ }
307
+ get lastWriteError() {
308
+ return this.lastError;
309
+ }
310
+ recentEntries(limit = this.recentLimit) {
311
+ return this.recent.slice(-Math.max(0, limit));
312
+ }
313
+ debug(event, message, context) {
314
+ return this.write({
315
+ level: "debug",
316
+ event,
317
+ message,
318
+ ...context ? { context } : {}
319
+ });
320
+ }
321
+ info(event, message, context) {
322
+ return this.write({
323
+ level: "info",
324
+ event,
325
+ message,
326
+ ...context ? { context } : {}
327
+ });
328
+ }
329
+ warn(event, message, error, context) {
330
+ return this.write({
331
+ level: "warn",
332
+ event,
333
+ message,
334
+ ...error !== void 0 ? { error } : {},
335
+ ...context ? { context } : {}
336
+ });
337
+ }
338
+ error(event, message, error, context) {
339
+ return this.write({
340
+ level: "error",
341
+ event,
342
+ message,
343
+ ...error !== void 0 ? { error } : {},
344
+ ...context ? { context } : {}
345
+ });
346
+ }
347
+ fault(event, message, error, context) {
348
+ return this.write({
349
+ level: "fault",
350
+ event,
351
+ message,
352
+ ...error !== void 0 ? { error } : {},
353
+ ...context ? { context } : {}
354
+ });
355
+ }
356
+ child(context) {
357
+ return createChildPluginLogger(this, context);
358
+ }
359
+ write(input) {
360
+ if (!pluginLogLevelEnabled(input.level, this.minimum) || this.writing) return false;
361
+ const at = this.now();
362
+ let entry;
363
+ let line;
364
+ try {
365
+ entry = buildPluginLogEntry(input, {
366
+ at,
367
+ source: this.source,
368
+ surface: "host",
369
+ profile: this.profile,
370
+ pid: this.pid,
371
+ correlation: correlationStorage.getStore()
372
+ });
373
+ line = `${JSON.stringify(entry)}\n`;
374
+ } catch {
375
+ return false;
376
+ }
377
+ this.pushRecent(entry);
378
+ if (!this.directory || !this.file) return true;
379
+ this.writing = true;
380
+ try {
381
+ this.openManagedDirectory();
382
+ this.prepare(at);
383
+ if (this.size > 0 && (this.size + Buffer.byteLength(line) > this.maxFileBytes || at.getTime() - this.openedAt >= this.maxAgeMs)) this.rotate(at);
384
+ appendFileSync(this.file, line, {
385
+ encoding: "utf8",
386
+ mode: 384
387
+ });
388
+ this.size += Buffer.byteLength(line);
389
+ this.prune(at);
390
+ this.lastError = null;
391
+ this.loggedWriteFailure = false;
392
+ return true;
393
+ } catch (error) {
394
+ const detail = sanitizePluginLogText(error instanceof Error ? error.message : String(error), 500);
395
+ this.lastError = `The plugin diagnostic log could not be written: ${detail}`;
396
+ this.initialized = false;
397
+ if (!this.loggedWriteFailure) {
398
+ this.loggedWriteFailure = true;
399
+ try {
400
+ process.stderr.write(`DSH Plugin (${this.source}): ${this.lastError}. The original operation continues unchanged.\n`);
401
+ } catch {}
402
+ }
403
+ return false;
404
+ } finally {
405
+ this.writing = false;
406
+ }
407
+ }
408
+ pushRecent(entry) {
409
+ this.recent.push(entry);
410
+ const overflow = this.recent.length - this.recentLimit;
411
+ if (overflow > 0) this.recent.splice(0, overflow);
412
+ }
413
+ prepare(at) {
414
+ if (this.initialized || !this.file) return;
415
+ this.openManagedDirectory();
416
+ const stat = this.regularFile(this.file);
417
+ this.size = stat?.size ?? 0;
418
+ const stamps = stat ? [stat.birthtimeMs, stat.mtimeMs].filter((value) => value > 0) : [];
419
+ this.openedAt = stamps.length ? Math.min(...stamps) : at.getTime();
420
+ this.initialized = true;
421
+ }
422
+ rotate(at) {
423
+ if (!this.directory || !this.file) return;
424
+ const stamp = at.toISOString().replace(/:/g, "-");
425
+ const name = `${this.source}.${stamp}.${this.pid}.${this.sequence++}.jsonl`;
426
+ if (!ARCHIVE_RE.test(name)) throw new Error("Diagnostic log archive name is invalid.");
427
+ const archive = join(this.directory, name);
428
+ if (lexists(archive)) throw new Error("Diagnostic log archive already exists.");
429
+ try {
430
+ renameSync(this.file, archive);
431
+ } catch (error) {
432
+ if (error.code === "ENOENT") {
433
+ this.size = 0;
434
+ this.openedAt = at.getTime();
435
+ return;
436
+ }
437
+ throw error;
438
+ }
439
+ this.size = 0;
440
+ this.openedAt = at.getTime();
441
+ }
442
+ rotateStaleActive(at) {
443
+ if (!this.file) return;
444
+ const stat = this.regularFile(this.file);
445
+ if (!stat || stat.size === 0) return;
446
+ if (at.getTime() - stat.mtimeMs < this.maxAgeMs) return;
447
+ this.rotate(at);
448
+ }
449
+ prune(at) {
450
+ if (!this.directory) return;
451
+ this.rotateStaleActive(at);
452
+ const archives = [];
453
+ let total = 0;
454
+ for (const name of readdirSync(this.directory)) {
455
+ if (!ARCHIVE_RE.test(name) || !name.startsWith(`${this.source}.`)) continue;
456
+ const file = join(this.directory, name);
457
+ const stat = lstatSync(file);
458
+ if (stat.isSymbolicLink() || !stat.isFile()) throw new Error("Diagnostic log archive is not a regular file.");
459
+ if (at.getTime() - stat.mtimeMs >= this.maxAgeMs) {
460
+ try {
461
+ unlinkSync(file);
462
+ } catch {}
463
+ continue;
464
+ }
465
+ archives.push({
466
+ name,
467
+ size: stat.size,
468
+ mtime: stat.mtimeMs
469
+ });
470
+ total += stat.size;
471
+ }
472
+ archives.sort((left, right) => left.mtime - right.mtime || left.name.localeCompare(right.name));
473
+ while (total > this.maxArchiveBytes && archives.length) {
474
+ const oldest = archives.shift();
475
+ try {
476
+ unlinkSync(join(this.directory, oldest.name));
477
+ } catch {}
478
+ total -= oldest.size;
479
+ }
480
+ }
481
+ openManagedDirectory() {
482
+ if (!this.directory) return;
483
+ if (this.anchor) {
484
+ if ((existsSync(this.anchor) ? lstatSync(this.anchor) : void 0)?.isSymbolicLink()) throw new Error("Diagnostic log anchor cannot be a symbolic link.");
485
+ }
486
+ mkdirSync(this.directory, {
487
+ recursive: true,
488
+ mode: 448
489
+ });
490
+ const stat = lstatSync(this.directory);
491
+ if (stat.isSymbolicLink() || !stat.isDirectory()) throw new Error("Diagnostic log directory is not a real directory.");
492
+ const realDir = realpathSync(this.directory);
493
+ if (this.anchor) {
494
+ if (!isInside(realpathSync(this.anchor), realDir)) throw new Error("Diagnostic log directory escaped its managed root.");
495
+ }
496
+ }
497
+ regularFile(file) {
498
+ try {
499
+ const stat = lstatSync(file);
500
+ if (stat.isSymbolicLink() || !stat.isFile()) throw new Error("Diagnostic log is not a regular file.");
501
+ const expected = join(realpathSync(dirname(file)), basename(file));
502
+ if (!samePath(realpathSync(file), expected)) throw new Error("Diagnostic log path could not be verified.");
503
+ return stat;
504
+ } catch (error) {
505
+ if (error.code === "ENOENT") return void 0;
506
+ throw error;
507
+ }
508
+ }
509
+ };
510
+ function createPluginLogger(ctxOrSource, maybeSource, maybeOptions) {
511
+ const hasCtx = typeof ctxOrSource !== "string";
512
+ const ctx = hasCtx ? ctxOrSource : void 0;
513
+ const source = hasCtx ? String(maybeSource ?? "dsh-plugin") : ctxOrSource;
514
+ const options = (hasCtx ? maybeOptions : maybeSource) ?? {};
515
+ const normalized = normalizePluginLogSource(source);
516
+ const target = resolveHostLogTarget(ctx, options);
517
+ let backing;
518
+ if (Boolean(target.directory && options.minimumLevel === void 0 && options.now === void 0 && options.pid === void 0 && options.maxFileBytes === void 0 && options.maxArchiveBytes === void 0 && options.maxAgeMs === void 0) && target.directory) {
519
+ const key = resolve(target.directory, `${normalized}.jsonl`);
520
+ let shared = processFileLoggers.get(key);
521
+ if (!shared) {
522
+ shared = new RotatingPluginLogger(normalized, {
523
+ ...options,
524
+ directory: target.directory,
525
+ anchor: target.anchor,
526
+ profile: target.profile
527
+ });
528
+ processFileLoggers.set(key, shared);
529
+ }
530
+ backing = shared;
531
+ } else backing = new RotatingPluginLogger(normalized, {
532
+ ...options,
533
+ ...target.directory ? {
534
+ directory: target.directory,
535
+ anchor: target.anchor
536
+ } : {},
537
+ ...target.profile ? { profile: target.profile } : {}
538
+ });
539
+ if (options.forwardHostWarnError === false || !ctx || typeof ctx !== "object") return backing;
540
+ return wrapWithHostForwarding(backing, ctx);
541
+ }
542
+ function installPluginLogger(ctx, source, options = {}) {
543
+ const logger = createPluginLogger(ctx, source, options);
544
+ logger.info("plugin.loaded", "Host plugin initialized.", options.loadedContext);
545
+ const host = ctx && typeof ctx === "object" ? ctx : void 0;
546
+ if (typeof host?.effect === "function") try {
547
+ host.effect(() => () => {
548
+ logger.debug("plugin.disposed", "Host plugin disposed.");
549
+ }, `${logger.source}.logger`);
550
+ } catch {}
551
+ return logger;
552
+ }
553
+ function wrapWithHostForwarding(backing, host) {
554
+ const forward = (input) => {
555
+ if (input.level !== "warn" && input.level !== "error" && input.level !== "fault") return;
556
+ try {
557
+ const hostLogger = host.logger;
558
+ if (!hostLogger) return;
559
+ const detail = input.error instanceof Error ? `${input.message} (${input.error.message})` : input.error !== void 0 ? `${input.message} (${String(input.error)})` : input.message;
560
+ const text = sanitizePluginLogText(`${backing.source} [${input.event}]: ${detail}`, 1e3);
561
+ if (input.level === "warn") {
562
+ if (typeof hostLogger.warn === "function") hostLogger.warn(text);
563
+ } else if (typeof hostLogger.error === "function") hostLogger.error(text);
564
+ else if (typeof hostLogger.warn === "function") hostLogger.warn(text);
565
+ } catch {}
566
+ };
567
+ const wrapped = {
568
+ get source() {
569
+ return backing.source;
570
+ },
571
+ get file() {
572
+ return backing.file;
573
+ },
574
+ get lastWriteError() {
575
+ return backing.lastWriteError;
576
+ },
577
+ debug: (event, message, context) => backing.debug(event, message, context),
578
+ info: (event, message, context) => backing.info(event, message, context),
579
+ warn: (event, message, error, context) => {
580
+ const wrote = backing.warn(event, message, error, context);
581
+ forward({
582
+ level: "warn",
583
+ event,
584
+ message,
585
+ error,
586
+ context
587
+ });
588
+ return wrote;
589
+ },
590
+ error: (event, message, error, context) => {
591
+ const wrote = backing.error(event, message, error, context);
592
+ forward({
593
+ level: "error",
594
+ event,
595
+ message,
596
+ error,
597
+ context
598
+ });
599
+ return wrote;
600
+ },
601
+ fault: (event, message, error, context) => {
602
+ const wrote = backing.fault(event, message, error, context);
603
+ forward({
604
+ level: "fault",
605
+ event,
606
+ message,
607
+ error,
608
+ context
609
+ });
610
+ return wrote;
611
+ },
612
+ write: (input) => {
613
+ const wrote = backing.write(input);
614
+ forward(input);
615
+ return wrote;
616
+ },
617
+ child: (context) => createChildPluginLogger(wrapped, context)
618
+ };
619
+ return wrapped;
620
+ }
621
+ function nextPluginOperationId(prefix = "op") {
622
+ return `${normalizePluginLogSource(prefix)}-${process.pid}-${++operationSequence}`;
623
+ }
624
+ function withPluginLogContext(context, action) {
625
+ const merged = mergePluginLogContext(correlationStorage.getStore(), context) ?? {};
626
+ return correlationStorage.run(merged, action);
627
+ }
628
+ function lexists(file) {
629
+ try {
630
+ lstatSync(file);
631
+ return true;
632
+ } catch (error) {
633
+ if (error.code === "ENOENT") return false;
634
+ throw error;
635
+ }
636
+ }
637
+ function pathFromBaseUrl(baseUrl) {
638
+ try {
639
+ return baseUrl.includes("://") ? resolve(fileURLToPath(baseUrl)) : resolve(baseUrl);
640
+ } catch {
641
+ return null;
642
+ }
643
+ }
644
+ function samePath(left, right) {
645
+ const a = resolve(left);
646
+ const b = resolve(right);
647
+ return process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
648
+ }
649
+ function isInside(root, target) {
650
+ const rootResolved = resolve(root);
651
+ const targetResolved = resolve(target);
652
+ if (samePath(rootResolved, targetResolved)) return true;
653
+ const prefix = rootResolved.endsWith(sep) ? rootResolved : rootResolved + sep;
654
+ return process.platform === "win32" ? targetResolved.toLowerCase().startsWith(prefix.toLowerCase()) : targetResolved.startsWith(prefix);
655
+ }
656
+ //#endregion
4
657
  //#region ../dsh-plugin-kit/src/host-rpc.ts
5
658
  /** The shared authenticated HTTP boundary. Each plugin still owns its RPC envelope semantics. */
6
659
  function registerHostRpcTransport(ctx, channel, dispatch, options = {}) {
7
660
  const bodyLimit = options.maxBodyBytes ?? 65536;
8
661
  if (!Number.isSafeInteger(bodyLimit) || bodyLimit <= 0) throw new Error("Invalid RPC body limit");
9
662
  const isEndpoint = options.isEndpoint ?? ((endpoint) => /^[a-z.]+$/.test(endpoint));
663
+ const log = options.logger;
10
664
  return ctx.webServer.register({
11
665
  kind: "prefix",
12
666
  path: channel,
@@ -17,29 +671,54 @@ function registerHostRpcTransport(ctx, channel, dispatch, options = {}) {
17
671
  else res.writeHead(status);
18
672
  res.end(override ?? options.errorBody?.(status, error) ?? (status === 500 ? "rpc request failed" : ""));
19
673
  };
674
+ let endpoint = "";
20
675
  try {
21
676
  if (typeof ctx.connection.requestRejection !== "function") {
677
+ log?.error("rpc.admission.unavailable", "Host RPC admission policy is unavailable.", void 0, {
678
+ channel,
679
+ status: 503
680
+ });
22
681
  reject(503);
23
682
  return;
24
683
  }
25
684
  const rejection = ctx.connection.requestRejection(req);
26
685
  if (rejection !== void 0) {
686
+ log?.warn("rpc.admission.rejected", "Host RPC request rejected by connection policy.", void 0, {
687
+ channel,
688
+ status: rejection
689
+ });
27
690
  reject(rejection);
28
691
  return;
29
692
  }
30
693
  const pathname = new URL(req.url ?? "/", "http://dsh.internal").pathname;
31
- const endpoint = pathname.startsWith(`${channel}/`) ? pathname.slice(channel.length + 1) : "";
694
+ endpoint = pathname.startsWith(`${channel}/`) ? pathname.slice(channel.length + 1) : "";
32
695
  if (req.method !== "POST" || !isEndpoint(endpoint)) {
696
+ log?.debug("rpc.route.rejected", "Host RPC route or method rejected.", {
697
+ channel,
698
+ endpoint: endpoint || null,
699
+ method: req.method ?? null,
700
+ status: 404
701
+ });
33
702
  reject(404);
34
703
  return;
35
704
  }
36
705
  const contentType = String(req.headers["content-type"] ?? "").split(";", 1)[0]?.trim();
37
706
  if ((options.caseInsensitiveContentType ? contentType.toLowerCase() : contentType) !== "application/json") {
707
+ log?.debug("rpc.content-type.rejected", "Host RPC content-type rejected.", {
708
+ channel,
709
+ endpoint,
710
+ status: 415
711
+ });
38
712
  reject(415);
39
713
  return;
40
714
  }
41
715
  const guarded = options.beforeBody?.(req, endpoint);
42
716
  if (guarded) {
717
+ log?.debug("rpc.guard.rejected", "Host RPC request rejected before body read.", {
718
+ channel,
719
+ endpoint,
720
+ status: guarded.status
721
+ });
43
722
  reject(guarded.status, void 0, guarded.body);
44
723
  return;
45
724
  }
@@ -53,6 +732,11 @@ function registerHostRpcTransport(ctx, channel, dispatch, options = {}) {
53
732
  const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
54
733
  size += buffer.length;
55
734
  if (size > bodyLimit) {
735
+ log?.warn("rpc.body.too-large", "Host RPC request exceeded body limit.", void 0, {
736
+ channel,
737
+ endpoint,
738
+ maxBodyBytes: bodyLimit
739
+ });
56
740
  reject(413);
57
741
  req.destroy();
58
742
  return;
@@ -63,11 +747,26 @@ function registerHostRpcTransport(ctx, channel, dispatch, options = {}) {
63
747
  try {
64
748
  body = JSON.parse(Buffer.concat(chunks).toString("utf8") || (options.emptyBodyAsNull ? "null" : ""));
65
749
  } catch {
750
+ log?.debug("rpc.body.invalid-json", "Host RPC request body is not valid JSON.", {
751
+ channel,
752
+ endpoint,
753
+ status: 400
754
+ });
66
755
  reject(400);
67
756
  return;
68
757
  }
69
- await dispatch(endpoint, body, controller.signal, res);
758
+ if (log) await withPluginLogContext({
759
+ rpcChannel: channel,
760
+ rpcEndpoint: endpoint,
761
+ operationId: nextPluginOperationId("rpc")
762
+ }, () => dispatch(endpoint, body, controller.signal, res));
763
+ else await dispatch(endpoint, body, controller.signal, res);
70
764
  } catch (error) {
765
+ log?.error("rpc.dispatch.failed", "Unhandled host RPC handler error.", error, {
766
+ channel,
767
+ endpoint: endpoint || null,
768
+ status: 500
769
+ });
71
770
  reject(500, error);
72
771
  }
73
772
  }
@@ -88,19 +787,41 @@ function sendHostRpcResponse(response, rpcId, result, noStore = true) {
88
787
  }
89
788
  /** Safe host RPC registration. Missing requestRejection fails closed. */
90
789
  function registerHostRpc(ctx, channel, call, options = {}) {
790
+ const log = options.logger;
91
791
  return registerHostRpcTransport(ctx, channel, async (endpoint, body, signal, res) => {
92
792
  if (!body || typeof body !== "object" || Array.isArray(body)) {
793
+ log?.debug("rpc.envelope.invalid", "Host RPC envelope must be a JSON object.", {
794
+ channel,
795
+ endpoint
796
+ });
93
797
  res.writeHead(400);
94
798
  res.end();
95
799
  return;
96
800
  }
97
801
  const request = body;
98
802
  if (request.type !== "client-request" || request.method !== endpoint || typeof request.rpcId !== "string" || options.requirePayload && !Object.hasOwn(request, "payload")) {
803
+ log?.debug("rpc.envelope.invalid", "Host RPC envelope failed validation.", {
804
+ channel,
805
+ endpoint
806
+ });
99
807
  res.writeHead(400);
100
808
  res.end();
101
809
  return;
102
810
  }
811
+ const started = Date.now();
103
812
  const raw = await call(endpoint, request.payload, signal);
813
+ const durationMs = Date.now() - started;
814
+ if (raw.ok) log?.debug("rpc.call.completed", "Host RPC call succeeded.", {
815
+ channel,
816
+ endpoint,
817
+ durationMs
818
+ });
819
+ else log?.warn("rpc.call.error", "Host RPC call returned an application error.", void 0, {
820
+ channel,
821
+ endpoint,
822
+ code: raw.error?.code ?? null,
823
+ durationMs
824
+ });
104
825
  const result = raw.ok ? raw : {
105
826
  ...raw,
106
827
  error: {
@@ -112,1102 +833,6 @@ function registerHostRpc(ctx, channel, call, options = {}) {
112
833
  }, options);
113
834
  }
114
835
  //#endregion
115
- //#region src/core/formula.js
116
- const FUNCTIONS = {
117
- min: {
118
- args: [1, Infinity],
119
- run: (...values) => Math.min(...values.map(num))
120
- },
121
- max: {
122
- args: [1, Infinity],
123
- run: (...values) => Math.max(...values.map(num))
124
- },
125
- abs: {
126
- args: [1, 1],
127
- run: (value) => Math.abs(num(value))
128
- },
129
- floor: {
130
- args: [1, 1],
131
- run: (value) => Math.floor(num(value))
132
- },
133
- ceil: {
134
- args: [1, 1],
135
- run: (value) => Math.ceil(num(value))
136
- },
137
- sqrt: {
138
- args: [1, 1],
139
- run: (value) => Math.sqrt(num(value))
140
- },
141
- pow: {
142
- args: [2, 2],
143
- run: (base, power) => num(base) ** num(power)
144
- },
145
- round: {
146
- args: [1, 2],
147
- run: (value, digits = 0) => roundTo(num(value), digitsOf(digits))
148
- },
149
- clamp: {
150
- args: [3, 3],
151
- run: (value, low, high) => Math.min(Math.max(num(value), num(low)), num(high))
152
- },
153
- fixed: {
154
- args: [1, 2],
155
- run: (value, digits = 0) => roundTo(num(value), digitsOf(digits)).toFixed(digitsOf(digits))
156
- },
157
- format: {
158
- args: [1, 2],
159
- run: (value, digits = 0) => grouped(num(value), digitsOf(digits))
160
- },
161
- percent: {
162
- args: [1, 2],
163
- run: (value, digits = 0) => `${grouped(num(value) * 100, digitsOf(digits))}%`
164
- }
165
- };
166
- const FORMULA_FUNCTIONS = Object.keys(FUNCTIONS);
167
- const CONSTANTS = /* @__PURE__ */ new Map([["true", true], ["false", false]]);
168
- const MAX_NESTING = 64;
169
- var FormulaError = class extends Error {};
170
- const fail$1 = (message) => {
171
- throw new FormulaError(message);
172
- };
173
- function num(value) {
174
- if (typeof value === "number") return value;
175
- if (typeof value === "boolean") return value ? 1 : 0;
176
- const parsed = Number(value);
177
- if (typeof value === "string" && value.trim() !== "" && !Number.isNaN(parsed)) return parsed;
178
- return fail$1(`${JSON.stringify(value)} is not a number`);
179
- }
180
- const digitsOf = (digits) => {
181
- const value = num(digits);
182
- if (!Number.isInteger(value) || value < 0 || value > 10) fail$1("digits must be a whole number from 0 to 10");
183
- return value;
184
- };
185
- const roundTo = (value, digits) => {
186
- const scale = 10 ** digits;
187
- return Math.round(value * scale) / scale;
188
- };
189
- function grouped(value, digits) {
190
- if (!Number.isFinite(value)) return String(value);
191
- const [whole, part] = Math.abs(roundTo(value, digits)).toFixed(digits).split(".");
192
- return `${value < 0 && Number(whole + (part ?? "")) !== 0 ? "-" : ""}${whole.replace(/\B(?=(\d{3})+(?!\d))/g, ",")}${part ? `.${part}` : ""}`;
193
- }
194
- const IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;
195
- const isName = (value) => typeof value === "string" && IDENT.test(value);
196
- const OPERATORS = [
197
- "==",
198
- "!=",
199
- "<=",
200
- ">=",
201
- "&&",
202
- "||",
203
- "+",
204
- "-",
205
- "*",
206
- "/",
207
- "%",
208
- "<",
209
- ">",
210
- "!",
211
- "?",
212
- ":",
213
- "(",
214
- ")",
215
- ","
216
- ];
217
- function tokenize(source) {
218
- const tokens = [];
219
- let at = 0;
220
- while (at < source.length) {
221
- const char = source[at];
222
- if (/\s/.test(char)) {
223
- at += 1;
224
- continue;
225
- }
226
- const number = /^(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/.exec(source.slice(at));
227
- if (number) {
228
- tokens.push({
229
- kind: "number",
230
- value: Number(number[0]),
231
- at
232
- });
233
- at += number[0].length;
234
- continue;
235
- }
236
- const name = /^[A-Za-z_][A-Za-z0-9_]*/.exec(source.slice(at));
237
- if (name) {
238
- tokens.push({
239
- kind: "name",
240
- value: name[0],
241
- at
242
- });
243
- at += name[0].length;
244
- continue;
245
- }
246
- if (char === "\"" || char === "'") {
247
- let end = at + 1;
248
- let text = "";
249
- while (end < source.length && source[end] !== char) {
250
- if (source[end] === "\\" && end + 1 < source.length) {
251
- text += source[end + 1];
252
- end += 2;
253
- continue;
254
- }
255
- text += source[end];
256
- end += 1;
257
- }
258
- if (end >= source.length) fail$1(`unclosed string at ${at + 1}`);
259
- tokens.push({
260
- kind: "string",
261
- value: text,
262
- at
263
- });
264
- at = end + 1;
265
- continue;
266
- }
267
- const operator = OPERATORS.find((op) => source.startsWith(op, at));
268
- if (operator === void 0) fail$1(`unexpected ${JSON.stringify(char)} at ${at + 1}`);
269
- tokens.push({
270
- kind: "op",
271
- value: operator,
272
- at
273
- });
274
- at += operator.length;
275
- }
276
- return tokens;
277
- }
278
- const BINARY = [
279
- ["||"],
280
- ["&&"],
281
- ["==", "!="],
282
- [
283
- "<",
284
- "<=",
285
- ">",
286
- ">="
287
- ],
288
- ["+", "-"],
289
- [
290
- "*",
291
- "/",
292
- "%"
293
- ]
294
- ];
295
- function parse(source) {
296
- const tokens = tokenize(source);
297
- let index = 0;
298
- let depth = 0;
299
- const peek = () => tokens[index];
300
- const isOp = (value) => peek()?.kind === "op" && peek().value === value;
301
- const expect = (value) => {
302
- if (!isOp(value)) fail$1(peek() ? `expected "${value}" at ${peek().at + 1}` : `expected "${value}" at the end`);
303
- index += 1;
304
- };
305
- const nest = (read) => {
306
- depth += 1;
307
- if (depth > MAX_NESTING) fail$1(`nested more than ${MAX_NESTING} levels`);
308
- const node = read();
309
- depth -= 1;
310
- return node;
311
- };
312
- const ternary = () => nest(() => {
313
- const test = binary(0);
314
- if (!isOp("?")) return test;
315
- index += 1;
316
- const yes = ternary();
317
- expect(":");
318
- return {
319
- type: "if",
320
- test,
321
- yes,
322
- no: ternary()
323
- };
324
- });
325
- const binary = (level) => {
326
- if (level === BINARY.length) return unary();
327
- let left = binary(level + 1);
328
- while (peek()?.kind === "op" && BINARY[level].includes(peek().value)) {
329
- const op = peek().value;
330
- index += 1;
331
- left = {
332
- type: "binary",
333
- op,
334
- left,
335
- right: binary(level + 1)
336
- };
337
- }
338
- return left;
339
- };
340
- const unary = () => {
341
- if (isOp("-") || isOp("!") || isOp("+")) {
342
- const op = peek().value;
343
- index += 1;
344
- return nest(() => ({
345
- type: "unary",
346
- op,
347
- value: unary()
348
- }));
349
- }
350
- return primary();
351
- };
352
- const primary = () => {
353
- const token = peek();
354
- if (token === void 0) fail$1("the formula ends too early");
355
- index += 1;
356
- if (token.kind === "number" || token.kind === "string") return {
357
- type: "value",
358
- value: token.value
359
- };
360
- if (token.kind === "op" && token.value === "(") {
361
- const inner = ternary();
362
- expect(")");
363
- return inner;
364
- }
365
- if (token.kind !== "name") fail$1(`unexpected "${token.value}" at ${token.at + 1}`);
366
- if (!isOp("(")) return CONSTANTS.has(token.value) ? {
367
- type: "value",
368
- value: CONSTANTS.get(token.value)
369
- } : {
370
- type: "name",
371
- name: token.value
372
- };
373
- const fn = Object.hasOwn(FUNCTIONS, token.value) ? FUNCTIONS[token.value] : void 0;
374
- if (fn === void 0) fail$1(`unknown function "${token.value}"; allowed: ${FORMULA_FUNCTIONS.join(", ")}`);
375
- index += 1;
376
- const args = [];
377
- if (!isOp(")")) {
378
- args.push(ternary());
379
- while (isOp(",")) {
380
- index += 1;
381
- args.push(ternary());
382
- }
383
- }
384
- expect(")");
385
- if (args.length < fn.args[0] || args.length > fn.args[1]) fail$1(`${token.value}() takes ${fn.args[0] === fn.args[1] ? fn.args[0] : fn.args[1] === Infinity ? `at least ${fn.args[0]}` : `${fn.args[0]} or ${fn.args[1]}`} argument${fn.args[1] === 1 ? "" : "s"}; got ${args.length}`);
386
- return {
387
- type: "call",
388
- name: token.value,
389
- args
390
- };
391
- };
392
- if (tokens.length === 0) fail$1("the formula is empty");
393
- const tree = ternary();
394
- if (index < tokens.length) fail$1(`unexpected "${peek().value}" at ${peek().at + 1}`);
395
- return tree;
396
- }
397
- function namesIn(node, found = /* @__PURE__ */ new Set()) {
398
- if (node.type === "name") found.add(node.name);
399
- else if (node.type === "unary") namesIn(node.value, found);
400
- else if (node.type === "binary") {
401
- namesIn(node.left, found);
402
- namesIn(node.right, found);
403
- } else if (node.type === "if") {
404
- namesIn(node.test, found);
405
- namesIn(node.yes, found);
406
- namesIn(node.no, found);
407
- } else if (node.type === "call") for (const arg of node.args) namesIn(arg, found);
408
- return found;
409
- }
410
- function compileFormula(source) {
411
- if (typeof source !== "string") return { error: "expected a formula string" };
412
- try {
413
- const tree = parse(source);
414
- return {
415
- tree,
416
- names: [...namesIn(tree)]
417
- };
418
- } catch (error) {
419
- if (error instanceof FormulaError) return { error: error.message };
420
- throw error;
421
- }
422
- }
423
- function run(node, lookup) {
424
- switch (node.type) {
425
- case "value": return node.value;
426
- case "name": return lookup(node.name);
427
- case "unary": {
428
- const value = run(node.value, lookup);
429
- if (node.op === "!") return !truthy(value);
430
- return node.op === "-" ? -num(value) : num(value);
431
- }
432
- case "if": return truthy(run(node.test, lookup)) ? run(node.yes, lookup) : run(node.no, lookup);
433
- case "call": return FUNCTIONS[node.name].run(...node.args.map((arg) => run(arg, lookup)));
434
- default: return binary(node, lookup);
435
- }
436
- }
437
- const truthy = (value) => typeof value === "string" ? value !== "" : Boolean(value);
438
- function binary(node, lookup) {
439
- if (node.op === "&&") return truthy(run(node.left, lookup)) ? run(node.right, lookup) : false;
440
- if (node.op === "||") {
441
- const left = run(node.left, lookup);
442
- return truthy(left) ? left : run(node.right, lookup);
443
- }
444
- const left = run(node.left, lookup);
445
- const right = run(node.right, lookup);
446
- switch (node.op) {
447
- case "+": return typeof left === "string" || typeof right === "string" ? `${show(left)}${show(right)}` : num(left) + num(right);
448
- case "-": return num(left) - num(right);
449
- case "*": return num(left) * num(right);
450
- case "/": return num(left) / num(right);
451
- case "%": return num(left) % num(right);
452
- case "==": return left === right;
453
- case "!=": return left !== right;
454
- case "<": return num(left) < num(right);
455
- case "<=": return num(left) <= num(right);
456
- case ">": return num(left) > num(right);
457
- default: return num(left) >= num(right);
458
- }
459
- }
460
- function show(value) {
461
- if (typeof value === "number") return Number.isFinite(value) ? String(Number(value.toPrecision(12))) : String(value);
462
- return String(value);
463
- }
464
- function createScope(inputs, lets) {
465
- const done = /* @__PURE__ */ new Map();
466
- const busy = /* @__PURE__ */ new Set();
467
- const lookup = (name) => {
468
- if (inputs.has(name)) return inputs.get(name);
469
- if (done.has(name)) return done.get(name);
470
- const tree = lets.get(name);
471
- if (tree === void 0) fail$1(`unknown name "${name}"`);
472
- if (busy.has(name)) fail$1(`"${name}" refers to itself`);
473
- busy.add(name);
474
- try {
475
- const value = run(tree, lookup);
476
- done.set(name, value);
477
- return value;
478
- } finally {
479
- busy.delete(name);
480
- }
481
- };
482
- return lookup;
483
- }
484
- function evaluate(tree, lookup) {
485
- try {
486
- return { value: run(tree, lookup) };
487
- } catch (error) {
488
- if (error instanceof FormulaError) return { error: error.message };
489
- if (error instanceof RangeError) return { error: "the formula is too deep to work out" };
490
- throw error;
491
- }
492
- }
493
- //#endregion
494
- //#region src/core/views.js
495
- const LAYOUT = ["stack", "row"];
496
- const DISPLAY = [
497
- "text",
498
- "metric",
499
- "table",
500
- "chart",
501
- "progress",
502
- "callout",
503
- "sources"
504
- ];
505
- const INPUT_TYPES = [
506
- "slider",
507
- "choice",
508
- "toggle",
509
- "number"
510
- ];
511
- const ANSWERS = [
512
- "options",
513
- "confirm",
514
- "form",
515
- "submit"
516
- ];
517
- const COMPONENTS = [
518
- ...LAYOUT,
519
- ...DISPLAY,
520
- ...INPUT_TYPES,
521
- ...ANSWERS
522
- ];
523
- const TONES = [
524
- "good",
525
- "warn",
526
- "bad"
527
- ];
528
- const TEXT_TONES = ["muted"];
529
- const CALLOUT_TONES = [
530
- "info",
531
- "good",
532
- "warn",
533
- "bad"
534
- ];
535
- const CHART_KINDS = [
536
- "bar",
537
- "hbar",
538
- "line",
539
- "area",
540
- "donut"
541
- ];
542
- const INTERACTIVE = new Set(ANSWERS);
543
- const INPUTS = new Set(INPUT_TYPES);
544
- const PROPS = {
545
- stack: ["children", "let"],
546
- row: ["children", "let"],
547
- text: ["text", "tone"],
548
- metric: [
549
- "label",
550
- "value",
551
- "tone",
552
- "caption"
553
- ],
554
- table: ["columns", "rows"],
555
- chart: [
556
- "kind",
557
- "categories",
558
- "series",
559
- "unit",
560
- "caption"
561
- ],
562
- progress: [
563
- "label",
564
- "percent",
565
- "value",
566
- "max",
567
- "valueLabel",
568
- "tone"
569
- ],
570
- callout: ["text", "tone"],
571
- sources: ["items"],
572
- slider: [
573
- "id",
574
- "label",
575
- "min",
576
- "max",
577
- "step",
578
- "value",
579
- "unit"
580
- ],
581
- choice: [
582
- "id",
583
- "label",
584
- "options",
585
- "value"
586
- ],
587
- toggle: [
588
- "id",
589
- "label",
590
- "value"
591
- ],
592
- number: [
593
- "id",
594
- "label",
595
- "min",
596
- "max",
597
- "step",
598
- "value",
599
- "unit"
600
- ],
601
- options: [
602
- "id",
603
- "options",
604
- "multi"
605
- ],
606
- confirm: [
607
- "id",
608
- "confirmLabel",
609
- "cancelLabel"
610
- ],
611
- form: [
612
- "id",
613
- "fields",
614
- "submitLabel"
615
- ],
616
- submit: ["id", "label"]
617
- };
618
- const MAX_DEPTH = 8;
619
- const MAX_NODES = 64;
620
- const MAX_CHILDREN = 16;
621
- const MAX_TEXT = 2e3;
622
- const MAX_CELL = 200;
623
- const MAX_COLUMNS = 8;
624
- const MAX_ROWS = 50;
625
- const MAX_OPTIONS = 8;
626
- const MAX_FIELDS = 6;
627
- const SUMMARY_PART = 40;
628
- const SUMMARY_COUNT = 8;
629
- const RESERVED = /* @__PURE__ */ new Set(["true", "false"]);
630
- const VIEW_BYTE_LIMIT = 8192;
631
- const isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
632
- const quoted = (value) => JSON.stringify(value);
633
- const allowed = (list) => list.join(", ");
634
- const isNumber = (value) => typeof value === "number" && Number.isFinite(value);
635
- const isScalar = (value) => typeof value === "string" || typeof value === "boolean" || isNumber(value);
636
- const isFormula = (value) => isRecord(value) && Object.keys(value).length === 1 && Object.hasOwn(value, "=");
637
- function viewBytes(title, view) {
638
- return new TextEncoder().encode(`${JSON.stringify(view) ?? ""}${title ?? ""}`).length;
639
- }
640
- const NUMERIC = [
641
- "value",
642
- "percent",
643
- "min",
644
- "max",
645
- "step",
646
- "height"
647
- ];
648
- const numeric = (value) => typeof value === "string" && value.trim() !== "" && !Number.isNaN(Number(value)) ? Number(value) : value;
649
- function normalizeView(value) {
650
- if (Array.isArray(value)) return value.map(normalizeView);
651
- if (!isRecord(value)) return value;
652
- const keys = Object.keys(value);
653
- if (keys.length === 1 && keys[0] === "item") {
654
- const inner = normalizeView(value.item);
655
- if (Array.isArray(inner)) return inner;
656
- }
657
- const out = {};
658
- for (const key of keys) Object.defineProperty(out, key, {
659
- value: normalizeView(value[key]),
660
- writable: true,
661
- enumerable: true,
662
- configurable: true
663
- });
664
- for (const field of NUMERIC) if (Object.hasOwn(out, field)) out[field] = numeric(out[field]);
665
- if (Array.isArray(out.values)) out.values = out.values.map(numeric);
666
- return out;
667
- }
668
- function validateView(view) {
669
- const ctx = {
670
- ids: /* @__PURE__ */ new Set(),
671
- nodes: 0,
672
- interactive: false,
673
- inputs: /* @__PURE__ */ new Map(),
674
- lets: /* @__PURE__ */ new Map(),
675
- formulas: []
676
- };
677
- const error = checkNode(view, "view", 1, ctx) ?? checkFormulas(ctx);
678
- return error === void 0 ? {
679
- ok: true,
680
- interactive: ctx.interactive
681
- } : {
682
- ok: false,
683
- error
684
- };
685
- }
686
- function checkShownView(title, sent) {
687
- let bytes;
688
- try {
689
- bytes = viewBytes(title, sent);
690
- } catch {
691
- return { error: "The view is not JSON data." };
692
- }
693
- if (bytes > 8192) return { error: `The view is ${bytes} bytes; the limit is ${VIEW_BYTE_LIMIT}.` };
694
- if (title !== void 0 && title !== null && typeof title !== "string") return { error: "title: expected a string" };
695
- const named = String(title ?? "").trim();
696
- const view = normalizeView(sent);
697
- const checked = validateView(view);
698
- if (!checked.ok) return { error: checked.error };
699
- return {
700
- title: named,
701
- view,
702
- interactive: checked.interactive
703
- };
704
- }
705
- function checkNode(node, path, depth, ctx) {
706
- if (!isRecord(node)) return `${path}: expected a component object`;
707
- if (depth > MAX_DEPTH) return `${path}: ${depth} levels exceeds the limit of ${MAX_DEPTH}`;
708
- ctx.nodes += 1;
709
- if (ctx.nodes > MAX_NODES) return `${path}: ${ctx.nodes} components exceeds the limit of ${MAX_NODES}`;
710
- const type = node.type;
711
- if (!COMPONENTS.includes(type)) return `${path}.type: unknown component ${quoted(type)}; allowed: ${allowed(COMPONENTS)}`;
712
- const props = PROPS[type];
713
- for (const key of Object.keys(node)) {
714
- if (key === "type" || props.includes(key)) continue;
715
- return `${path}.${key}: unknown property ${quoted(key)}; allowed: ${allowed(props)}`;
716
- }
717
- if (INTERACTIVE.has(type)) ctx.interactive = true;
718
- switch (type) {
719
- case "stack":
720
- case "row": return checkLayout(node, path, depth, ctx);
721
- case "text": return checkText(node, path, ctx);
722
- case "metric": return checkMetric(node, path, ctx);
723
- case "table": return checkTable(node, path, ctx);
724
- case "chart": return checkChart(node, path, ctx);
725
- case "progress": return checkProgress(node, path, ctx);
726
- case "callout": return checkCallout(node, path, ctx);
727
- case "sources": return checkSources(node, path);
728
- case "slider":
729
- case "number": return checkNumberInput(node, path, ctx);
730
- case "choice": return checkChoice(node, path, ctx);
731
- case "toggle": return checkToggle(node, path, ctx);
732
- case "options": return checkOptions(node, path, ctx);
733
- case "confirm": return checkConfirm(node, path, ctx);
734
- case "form": return checkForm(node, path, ctx);
735
- default: return checkSubmit(node, path, ctx);
736
- }
737
- }
738
- function checkLayout(node, path, depth, ctx) {
739
- if (node.let !== void 0) {
740
- if (depth !== 1) return `${path}.let: only the outermost component can define names`;
741
- if (!isRecord(node.let)) return `${path}.let: expected an object of names to formulas`;
742
- for (const [name, source] of Object.entries(node.let)) {
743
- if (!isName(name) || RESERVED.has(name)) return `${path}.let.${name}: a name is letters, digits and _, and starts with a letter or _`;
744
- if (typeof source !== "string") return `${path}.let.${name}: expected a formula string`;
745
- ctx.lets.set(name, {
746
- source,
747
- path: `${path}.let.${name}`
748
- });
749
- }
750
- }
751
- if (!Array.isArray(node.children)) return `${path}.children: expected an array of 1 to ${MAX_CHILDREN} components`;
752
- if (node.children.length < 1 || node.children.length > MAX_CHILDREN) return `${path}.children: expected 1 to ${MAX_CHILDREN} children; got ${node.children.length}`;
753
- for (let index = 0; index < node.children.length; index += 1) {
754
- const error = checkNode(node.children[index], `${path}.children[${index}]`, depth + 1, ctx);
755
- if (error !== void 0) return error;
756
- }
757
- }
758
- function checkValue(node, key, path, ctx, kind, { required = false, max = MAX_TEXT } = {}) {
759
- const value = node[key];
760
- const at = `${path}.${key}`;
761
- if (value === void 0) return required ? `${at}: ${expected(kind)}` : void 0;
762
- if (isFormula(value)) {
763
- ctx.formulas.push({
764
- source: value["="],
765
- path: at,
766
- kind
767
- });
768
- return;
769
- }
770
- if (kind === "number") return isNumber(value) ? void 0 : `${at}: ${expected(kind)}`;
771
- if (Array.isArray(kind)) return kind.includes(value) ? void 0 : `${at}: unknown tone ${quoted(value)}; allowed: ${allowed(kind)}`;
772
- if (kind === "string") {
773
- if (typeof value !== "string") return `${at}: ${expected(kind)}`;
774
- return value.length > max ? `${at}: ${value.length} characters exceeds the limit of ${max}` : void 0;
775
- }
776
- if (!isScalar(value)) return `${at}: ${expected(kind)}`;
777
- }
778
- const expected = (kind) => kind === "number" ? "expected a number" : kind === "string" ? "expected a string" : Array.isArray(kind) ? `expected one of ${allowed(kind)}` : "expected a string or a number";
779
- function checkText(node, path, ctx) {
780
- return checkValue(node, "text", path, ctx, "string", { required: true }) ?? checkValue(node, "tone", path, ctx, TEXT_TONES);
781
- }
782
- function checkMetric(node, path, ctx) {
783
- if (typeof node.label !== "string") return `${path}.label: expected a string`;
784
- if (node.value === void 0) return `${path}.value: expected a string or a number`;
785
- if (typeof node.value === "number" && !Number.isFinite(node.value)) return `${path}.value: expected a finite number`;
786
- return checkValue(node, "value", path, ctx, "scalar") ?? checkValue(node, "tone", path, ctx, TONES) ?? checkValue(node, "caption", path, ctx, "string");
787
- }
788
- function checkTable(node, path, ctx) {
789
- if (!Array.isArray(node.columns)) return `${path}.columns: expected an array of at most ${MAX_COLUMNS} columns`;
790
- if (node.columns.length > MAX_COLUMNS) return `${path}.columns: ${node.columns.length} columns exceeds the limit of ${MAX_COLUMNS}`;
791
- for (let index = 0; index < node.columns.length; index += 1) {
792
- const column = node.columns[index];
793
- const at = `${path}.columns[${index}]`;
794
- if (!isRecord(column)) return `${at}: expected a column object`;
795
- for (const key of Object.keys(column)) if (key !== "key" && key !== "label") return `${at}.${key}: unknown property ${quoted(key)}; allowed: key, label`;
796
- if (typeof column.key !== "string" || column.key === "") return `${at}.key: expected a non-empty string`;
797
- if (typeof column.label !== "string") return `${at}.label: expected a string`;
798
- }
799
- if (!Array.isArray(node.rows)) return `${path}.rows: expected an array of at most ${MAX_ROWS} rows`;
800
- if (node.rows.length > MAX_ROWS) return `${path}.rows: ${node.rows.length} rows exceeds the limit of ${MAX_ROWS}`;
801
- for (let row = 0; row < node.rows.length; row += 1) {
802
- const cells = node.rows[row];
803
- if (!Array.isArray(cells)) return `${path}.rows[${row}]: expected an array of cells`;
804
- for (let column = 0; column < cells.length; column += 1) {
805
- const at = `${path}.rows[${row}][${column}]`;
806
- if (isFormula(cells[column])) {
807
- ctx.formulas.push({
808
- source: cells[column]["="],
809
- path: at,
810
- kind: "scalar"
811
- });
812
- continue;
813
- }
814
- const text = cellText(cells[column]);
815
- if (text.length > MAX_CELL) return `${at}: ${text.length} characters exceeds the limit of ${MAX_CELL}`;
816
- }
817
- }
818
- }
819
- function checkChart(node, path, ctx) {
820
- if (node.kind !== void 0 && !CHART_KINDS.includes(node.kind)) return `${path}.kind: unknown chart ${quoted(node.kind)}; allowed: ${allowed(CHART_KINDS)}`;
821
- if (!Array.isArray(node.categories) || node.categories.length === 0) return `${path}.categories: expected a non-empty array of labels`;
822
- for (let index = 0; index < node.categories.length; index += 1) {
823
- const label = node.categories[index];
824
- if (typeof label !== "string" && !isNumber(label)) return `${path}.categories[${index}]: expected a string`;
825
- }
826
- if (!Array.isArray(node.series) || node.series.length === 0) return `${path}.series: expected a non-empty array of { name?, values }`;
827
- if (node.kind === "donut" && node.series.length > 1) return `${path}.series: a donut takes one series; got ${node.series.length}`;
828
- for (let index = 0; index < node.series.length; index += 1) {
829
- const series = node.series[index];
830
- const at = `${path}.series[${index}]`;
831
- if (!isRecord(series)) return `${at}: expected a series object`;
832
- for (const key of Object.keys(series)) if (key !== "name" && key !== "values") return `${at}.${key}: unknown property ${quoted(key)}; allowed: name, values`;
833
- if (series.name !== void 0 && typeof series.name !== "string") return `${at}.name: expected a string`;
834
- if (!Array.isArray(series.values)) return `${at}.values: expected an array of numbers`;
835
- if (series.values.length !== node.categories.length) return `${at}.values: ${series.values.length} values for ${node.categories.length} categories`;
836
- for (let item = 0; item < series.values.length; item += 1) {
837
- const value = series.values[item];
838
- if (isFormula(value)) ctx.formulas.push({
839
- source: value["="],
840
- path: `${at}.values[${item}]`,
841
- kind: "number"
842
- });
843
- else if (!isNumber(value)) return `${at}.values[${item}]: expected a number`;
844
- }
845
- }
846
- if (node.unit !== void 0 && typeof node.unit !== "string") return `${path}.unit: expected a string`;
847
- if (node.caption !== void 0 && typeof node.caption !== "string") return `${path}.caption: expected a string`;
848
- }
849
- function checkProgress(node, path, ctx) {
850
- if (node.label !== void 0 && typeof node.label !== "string") return `${path}.label: expected a string`;
851
- if (node.percent === void 0 === (node.value === void 0)) return `${path}: give percent (0 to 100), or value with max`;
852
- if (node.percent !== void 0 && !isFormula(node.percent)) {
853
- if (typeof node.percent !== "number" || !Number.isFinite(node.percent) || node.percent < 0 || node.percent > 100) return `${path}.percent: expected a number from 0 to 100`;
854
- }
855
- if (node.value !== void 0 && node.max === void 0) return `${path}.max: expected a number with value`;
856
- if (node.value === void 0 && node.max !== void 0) return `${path}.value: expected a number with max`;
857
- return checkValue(node, "percent", path, ctx, "number") ?? checkValue(node, "value", path, ctx, "number") ?? checkValue(node, "max", path, ctx, "number") ?? checkValue(node, "valueLabel", path, ctx, "scalar") ?? checkValue(node, "tone", path, ctx, TONES);
858
- }
859
- function checkCallout(node, path, ctx) {
860
- return checkValue(node, "text", path, ctx, "string", { required: true }) ?? checkValue(node, "tone", path, ctx, CALLOUT_TONES);
861
- }
862
- function checkSources(node, path) {
863
- if (!Array.isArray(node.items) || node.items.length === 0) return `${path}.items: expected a non-empty array of { label, url }`;
864
- for (let index = 0; index < node.items.length; index += 1) {
865
- const item = node.items[index];
866
- const at = `${path}.items[${index}]`;
867
- if (!isRecord(item)) return `${at}: expected a { label, url } object`;
868
- for (const key of Object.keys(item)) if (key !== "label" && key !== "url") return `${at}.${key}: unknown property ${quoted(key)}; allowed: label, url`;
869
- if (typeof item.label !== "string" || item.label.trim() === "") return `${at}.label: expected a non-empty string`;
870
- if (typeof item.url !== "string" || !/^https?:\/\/[^\s]+$/i.test(item.url)) return `${at}.url: expected an http(s) address`;
871
- }
872
- }
873
- function checkInputId(node, path, ctx) {
874
- if (!isName(node.id) || RESERVED.has(node.id)) return `${path}.id: an input id is letters, digits and _, and starts with a letter or _, so formulas can use it`;
875
- if (ctx.ids.has(node.id)) return `${path}.id: duplicate id ${quoted(node.id)}`;
876
- ctx.ids.add(node.id);
877
- if (typeof node.label !== "string") return `${path}.label: expected a string`;
878
- }
879
- function checkNumberInput(node, path, ctx) {
880
- const idError = checkInputId(node, path, ctx);
881
- if (idError !== void 0) return idError;
882
- for (const key of [
883
- "min",
884
- "max",
885
- "step"
886
- ]) if (node[key] !== void 0 && !isNumber(node[key])) return `${path}.${key}: expected a number`;
887
- if (node.type === "slider" && (node.min === void 0 || node.max === void 0)) return `${path}: a slider needs min and max`;
888
- if (node.min !== void 0 && node.max !== void 0 && node.max <= node.min) return `${path}.max: expected more than min (${node.min})`;
889
- if (node.step !== void 0 && node.step <= 0) return `${path}.step: expected more than 0`;
890
- if (!isNumber(node.value)) return `${path}.value: expected a number`;
891
- if (node.min !== void 0 && node.value < node.min || node.max !== void 0 && node.value > node.max) return `${path}.value: expected a number from ${node.min ?? "-∞"} to ${node.max ?? "∞"}`;
892
- if (node.unit !== void 0 && typeof node.unit !== "string") return `${path}.unit: expected a string`;
893
- ctx.inputs.set(node.id, node.value);
894
- }
895
- function checkChoice(node, path, ctx) {
896
- const idError = checkInputId(node, path, ctx);
897
- if (idError !== void 0) return idError;
898
- if (!Array.isArray(node.options) || node.options.length === 0) return `${path}.options: expected a non-empty array of { label, value }`;
899
- const labels = /* @__PURE__ */ new Set();
900
- const values = /* @__PURE__ */ new Set();
901
- for (let index = 0; index < node.options.length; index += 1) {
902
- const option = node.options[index];
903
- const at = `${path}.options[${index}]`;
904
- if (!isRecord(option)) return `${at}: expected a { label, value } object`;
905
- for (const key of Object.keys(option)) if (key !== "label" && key !== "value") return `${at}.${key}: unknown property ${quoted(key)}; allowed: label, value`;
906
- if (typeof option.label !== "string" || option.label.trim() === "") return `${at}.label: expected a non-empty string`;
907
- if (labels.has(option.label)) return `${at}.label: duplicate label ${quoted(option.label)}`;
908
- labels.add(option.label);
909
- const value = option.value ?? option.label;
910
- if (!isScalar(value)) return `${at}.value: expected a string, a number or true/false`;
911
- if (values.has(value)) return `${at}.value: duplicate value ${quoted(value)}`;
912
- values.add(value);
913
- }
914
- const chosen = node.value ?? choiceValue(node.options[0]);
915
- if (!values.has(chosen)) return `${path}.value: expected one of the options' values`;
916
- ctx.inputs.set(node.id, chosen);
917
- }
918
- function checkToggle(node, path, ctx) {
919
- const idError = checkInputId(node, path, ctx);
920
- if (idError !== void 0) return idError;
921
- if (node.value !== void 0 && typeof node.value !== "boolean") return `${path}.value: expected true or false`;
922
- ctx.inputs.set(node.id, node.value === true);
923
- }
924
- function checkOptions(node, path, ctx) {
925
- const idError = checkId(node, path, ctx);
926
- if (idError !== void 0) return idError;
927
- if (node.multi !== void 0 && typeof node.multi !== "boolean") return `${path}.multi: expected a boolean`;
928
- if (!Array.isArray(node.options)) return `${path}.options: expected an array of 1 to ${MAX_OPTIONS} options`;
929
- if (node.options.length < 1 || node.options.length > MAX_OPTIONS) return `${path}.options: expected 1 to ${MAX_OPTIONS} options; got ${node.options.length}`;
930
- const labels = /* @__PURE__ */ new Set();
931
- for (let index = 0; index < node.options.length; index += 1) {
932
- const option = node.options[index];
933
- const at = `${path}.options[${index}]`;
934
- if (!isRecord(option)) return `${at}: expected an option object`;
935
- for (const key of Object.keys(option)) if (key !== "label" && key !== "description") return `${at}.${key}: unknown property ${quoted(key)}; allowed: label, description`;
936
- if (typeof option.label !== "string" || option.label.trim() === "") return `${at}.label: expected a non-empty string`;
937
- if (labels.has(option.label)) return `${at}.label: duplicate label ${quoted(option.label)}`;
938
- labels.add(option.label);
939
- if (option.description !== void 0 && typeof option.description !== "string") return `${at}.description: expected a string`;
940
- }
941
- }
942
- function checkConfirm(node, path, ctx) {
943
- const idError = checkId(node, path, ctx);
944
- if (idError !== void 0) return idError;
945
- if (node.confirmLabel !== void 0 && typeof node.confirmLabel !== "string") return `${path}.confirmLabel: expected a string`;
946
- if (node.cancelLabel !== void 0 && typeof node.cancelLabel !== "string") return `${path}.cancelLabel: expected a string`;
947
- }
948
- function checkForm(node, path, ctx) {
949
- const idError = checkId(node, path, ctx);
950
- if (idError !== void 0) return idError;
951
- if (node.submitLabel !== void 0 && typeof node.submitLabel !== "string") return `${path}.submitLabel: expected a string`;
952
- if (!Array.isArray(node.fields)) return `${path}.fields: expected an array of at most ${MAX_FIELDS} fields`;
953
- if (node.fields.length > MAX_FIELDS) return `${path}.fields: ${node.fields.length} fields exceeds the limit of ${MAX_FIELDS}`;
954
- const seen = /* @__PURE__ */ new Set();
955
- for (let index = 0; index < node.fields.length; index += 1) {
956
- const field = node.fields[index];
957
- const at = `${path}.fields[${index}]`;
958
- if (!isRecord(field)) return `${at}: expected a field object`;
959
- for (const key of Object.keys(field)) if (key !== "id" && key !== "label" && key !== "placeholder") return `${at}.${key}: unknown property ${quoted(key)}; allowed: id, label, placeholder`;
960
- if (typeof field.id !== "string" || field.id === "") return `${at}.id: expected a non-empty string`;
961
- if (field.id === "__proto__") return `${at}.id: "__proto__" cannot be an id`;
962
- if (seen.has(field.id)) return `${at}.id: duplicate field id ${quoted(field.id)}`;
963
- seen.add(field.id);
964
- if (typeof field.label !== "string") return `${at}.label: expected a string`;
965
- if (field.placeholder !== void 0 && typeof field.placeholder !== "string") return `${at}.placeholder: expected a string`;
966
- }
967
- }
968
- function checkSubmit(node, path, ctx) {
969
- const idError = checkId(node, path, ctx);
970
- if (idError !== void 0) return idError;
971
- if (node.label !== void 0 && typeof node.label !== "string") return `${path}.label: expected a string`;
972
- }
973
- function checkId(node, path, ctx) {
974
- if (typeof node.id !== "string" || node.id === "") return `${path}.id: expected a non-empty string`;
975
- if (node.id === "__proto__") return `${path}.id: "__proto__" cannot be an id`;
976
- if (ctx.ids.has(node.id)) return `${path}.id: duplicate id ${quoted(node.id)}`;
977
- ctx.ids.add(node.id);
978
- }
979
- function checkFormulas(ctx) {
980
- const known = /* @__PURE__ */ new Set([...ctx.inputs.keys(), ...ctx.lets.keys()]);
981
- const lets = /* @__PURE__ */ new Map();
982
- for (const [name, entry] of ctx.lets) {
983
- if (ctx.inputs.has(name)) return `${entry.path}: ${quoted(name)} is already an input id`;
984
- const compiled = compileFormula(entry.source);
985
- if (compiled.error !== void 0) return `${entry.path}: ${compiled.error}`;
986
- const unknown = compiled.names.find((item) => !known.has(item));
987
- if (unknown !== void 0) return `${entry.path}: unknown name ${quoted(unknown)}; known: ${allowed([...known]) || "(none)"}`;
988
- lets.set(name, compiled.tree);
989
- }
990
- const lookup = createScope(ctx.inputs, lets);
991
- for (const [name, entry] of ctx.lets) {
992
- const result = evaluate(lets.get(name), lookup);
993
- if (result.error !== void 0) return `${entry.path}: ${result.error}`;
994
- }
995
- for (const formula of ctx.formulas) {
996
- const compiled = compileFormula(formula.source);
997
- if (compiled.error !== void 0) return `${formula.path}: ${compiled.error}`;
998
- const unknown = compiled.names.find((item) => !known.has(item));
999
- if (unknown !== void 0) return `${formula.path}: unknown name ${quoted(unknown)}; known: ${allowed([...known]) || "(none)"}`;
1000
- const result = evaluate(compiled.tree, lookup);
1001
- if (result.error !== void 0) return `${formula.path}: ${result.error}`;
1002
- const problem = kindProblem(result.value, formula.kind);
1003
- if (problem !== void 0) return `${formula.path}: the formula gives ${quoted(result.value)}; ${problem}`;
1004
- }
1005
- }
1006
- function kindProblem(value, kind) {
1007
- if (kind === "number") return isNumber(value) ? void 0 : "expected a number";
1008
- if (Array.isArray(kind)) return kind.includes(value) ? void 0 : `expected one of ${allowed(kind)}`;
1009
- if (kind === "string") return isScalar(value) ? void 0 : "expected text";
1010
- return isScalar(value) ? void 0 : "expected a string or a number";
1011
- }
1012
- function cellText(value) {
1013
- if (typeof value === "string") return value;
1014
- const text = JSON.stringify(value);
1015
- return typeof text === "string" ? text : "";
1016
- }
1017
- const compiled = /* @__PURE__ */ new WeakMap();
1018
- const treeOf = (formula) => {
1019
- let entry = compiled.get(formula);
1020
- if (entry === void 0) {
1021
- entry = compileFormula(formula["="]);
1022
- compiled.set(formula, entry);
1023
- }
1024
- return entry;
1025
- };
1026
- const choiceValue = (option) => option?.value ?? option?.label;
1027
- function walk(node, visit) {
1028
- if (!isRecord(node)) return;
1029
- visit(node);
1030
- if (LAYOUT.includes(node.type) && Array.isArray(node.children)) for (const child of node.children) walk(child, visit);
1031
- }
1032
- function viewInputs(view) {
1033
- const found = [];
1034
- walk(view, (node) => {
1035
- if (INPUTS.has(node.type)) found.push(node);
1036
- });
1037
- return found;
1038
- }
1039
- function inputDefault(node) {
1040
- if (node.type === "toggle") return node.value === true;
1041
- if (node.type === "choice") return node.value ?? choiceValue(Array.isArray(node.options) ? node.options[0] : void 0);
1042
- return node.value;
1043
- }
1044
- function viewScope(view, values = {}) {
1045
- const inputs = /* @__PURE__ */ new Map();
1046
- for (const node of viewInputs(view)) inputs.set(node.id, Object.hasOwn(values, node.id) ? values[node.id] : inputDefault(node));
1047
- const lets = /* @__PURE__ */ new Map();
1048
- for (const [name, source] of Object.entries(isRecord(view?.let) ? view.let : {})) {
1049
- const entry = compileFormula(source);
1050
- if (entry.tree !== void 0) lets.set(name, entry.tree);
1051
- }
1052
- return createScope(inputs, lets);
1053
- }
1054
- function resolve(value, lookup, fallback = void 0) {
1055
- if (!isFormula(value)) return value;
1056
- const entry = treeOf(value);
1057
- if (entry.tree === void 0) return fallback;
1058
- const result = evaluate(entry.tree, lookup);
1059
- return result.error === void 0 ? result.value : fallback;
1060
- }
1061
- const asText = (value) => value === void 0 || value === null ? "" : show(value);
1062
- function clip(text) {
1063
- const flat = String(text).replace(/\s+/g, " ").trim();
1064
- return flat.length <= SUMMARY_PART ? flat : flat.slice(0, SUMMARY_PART);
1065
- }
1066
- const choiceLabel = (node, value) => (Array.isArray(node.options) ? node.options : []).find((option) => choiceValue(option) === value)?.label ?? asText(value);
1067
- function inputText(node, value) {
1068
- if (node.type === "toggle") return value === true ? "on" : "off";
1069
- if (node.type === "choice") return choiceLabel(node, value);
1070
- return `${asText(value)}${node.unit ? ` ${node.unit}` : ""}`;
1071
- }
1072
- function progressPercent(node, lookup) {
1073
- if (node.percent !== void 0) return Number(resolve(node.percent, lookup, 0));
1074
- const max = Number(resolve(node.max, lookup, 0));
1075
- return max > 0 ? Number(resolve(node.value, lookup, 0)) / max * 100 : 0;
1076
- }
1077
- function componentSummary(node, lookup) {
1078
- const value = (item) => asText(resolve(item, lookup, ""));
1079
- switch (node.type) {
1080
- case "text": return `text: ${value(node.text)}`;
1081
- case "metric": return `metric: ${node.label}=${value(node.value)}`;
1082
- case "table": return `table: ${(Array.isArray(node.columns) ? node.columns : []).map((column) => column?.label ?? column?.key ?? "").filter(Boolean).join("/")}`;
1083
- case "chart": {
1084
- const series = Array.isArray(node.series) ? node.series[0] : void 0;
1085
- const values = Array.isArray(series?.values) ? series.values : [];
1086
- const unit = node.unit ? ` ${node.unit}` : "";
1087
- return `chart: ${(Array.isArray(node.categories) ? node.categories : []).map((label, index) => `${label}=${value(values[index])}${unit}`).join(", ")}`;
1088
- }
1089
- case "progress": return `progress: ${node.label ? `${node.label} ` : ""}${Math.round(progressPercent(node, lookup))}%`;
1090
- case "callout": return `callout: ${value(node.text)}`;
1091
- case "sources": return `sources: ${(Array.isArray(node.items) ? node.items : []).map((item) => item?.label).filter(Boolean).join("/")}`;
1092
- case "slider":
1093
- case "number":
1094
- case "choice":
1095
- case "toggle": return `${node.type}: ${node.label}=${inputText(node, inputDefault(node))}`;
1096
- case "options": return `options: ${(Array.isArray(node.options) ? node.options : []).map((option) => option?.label).filter(Boolean).join("/")}`;
1097
- case "confirm": return "confirm";
1098
- case "form": return `form: ${(Array.isArray(node.fields) ? node.fields : []).map((field) => field?.label ?? field?.id).filter(Boolean).join("/")}`;
1099
- case "submit": return `submit: ${node.label ?? "Send"}`;
1100
- default: return;
1101
- }
1102
- }
1103
- function viewSummary(view) {
1104
- const parts = [];
1105
- const lookup = viewScope(view);
1106
- const visit = (node) => {
1107
- if (parts.length >= SUMMARY_COUNT || !isRecord(node)) return;
1108
- const line = componentSummary(node, lookup);
1109
- if (line !== void 0) parts.push(clip(line));
1110
- if (parts.length >= SUMMARY_COUNT || !LAYOUT.includes(node.type) || !Array.isArray(node.children)) return;
1111
- for (const child of node.children) visit(child);
1112
- };
1113
- visit(view);
1114
- return parts.join("; ");
1115
- }
1116
- function inputsSummary(view, inputs) {
1117
- if (!isRecord(inputs)) return "";
1118
- return viewInputs(view).filter((node) => Object.hasOwn(inputs, node.id)).map((node) => `${node.label || node.id}=${inputText(node, inputs[node.id])}`).join("; ");
1119
- }
1120
- function interactiveNodes(node, found = []) {
1121
- walk(node, (item) => {
1122
- if (INTERACTIVE.has(item.type)) found.push(item);
1123
- });
1124
- return found;
1125
- }
1126
- function validateViewAnswer(view, action, values) {
1127
- const nodes = interactiveNodes(view);
1128
- if (typeof action !== "string" || action === "") return { error: `action: expected an interactive component id; allowed: ${allowed(nodes.map((node) => node.id))}` };
1129
- const node = nodes.find((item) => item.id === action);
1130
- if (node === void 0) return { error: `action: no interactive component with id ${quoted(action)}; allowed: ${allowed(nodes.map((item) => item.id))}` };
1131
- if (!isRecord(values)) return { error: "values: expected an object" };
1132
- const inputs = validateInputs(view, values.inputs);
1133
- if (inputs.error !== void 0) return inputs;
1134
- const extra = inputs.inputs === void 0 ? {} : { inputs: inputs.inputs };
1135
- if (node.type === "submit") return { ...extra };
1136
- const own = node.type === "options" ? validateOptions(node, values) : node.type === "confirm" ? validateConfirm(values) : validateForm(node, values);
1137
- return own.error === void 0 ? {
1138
- ...own,
1139
- ...extra
1140
- } : own;
1141
- }
1142
- function validateInputs(view, sent) {
1143
- const nodes = viewInputs(view);
1144
- if (nodes.length === 0) return sent === void 0 ? {} : { error: "values.inputs: this view has no inputs" };
1145
- if (sent !== void 0 && !isRecord(sent)) return { error: "values.inputs: expected an object of input ids to values" };
1146
- const given = sent ?? {};
1147
- const ids = nodes.map((node) => node.id);
1148
- const unknown = Object.keys(given).find((key) => !ids.includes(key));
1149
- if (unknown !== void 0) return { error: `values.inputs.${unknown}: unknown input; allowed: ${allowed(ids)}` };
1150
- const inputs = {};
1151
- for (const node of nodes) {
1152
- const value = Object.hasOwn(given, node.id) ? given[node.id] : inputDefault(node);
1153
- const at = `values.inputs.${node.id}`;
1154
- if (node.type === "toggle" && typeof value !== "boolean") return { error: `${at}: expected true or false` };
1155
- if (node.type === "choice" && !(Array.isArray(node.options) ? node.options : []).some((option) => choiceValue(option) === value)) return { error: `${at}: expected one of the options' values` };
1156
- if (node.type === "slider" || node.type === "number") {
1157
- if (!isNumber(value)) return { error: `${at}: expected a number` };
1158
- if (node.min !== void 0 && value < node.min || node.max !== void 0 && value > node.max) return { error: `${at}: expected a number from ${node.min ?? "-∞"} to ${node.max ?? "∞"}` };
1159
- }
1160
- inputs[node.id] = value;
1161
- }
1162
- return { inputs };
1163
- }
1164
- function validateOptions(node, values) {
1165
- if (!Array.isArray(values.selected)) return { error: "values.selected: expected an array of option labels" };
1166
- const labels = new Set((node.options ?? []).map((option) => option?.label));
1167
- const picked = values.selected.filter((label) => typeof label === "string" && labels.has(label));
1168
- const selected = node.multi === true ? [...new Set(picked)] : picked.slice(0, 1);
1169
- if (selected.length === 0) return { error: "values.selected: select at least one option" };
1170
- return { selected };
1171
- }
1172
- function validateConfirm(values) {
1173
- if (typeof values.confirmed !== "boolean") return { error: "values.confirmed: expected a boolean" };
1174
- return { confirmed: values.confirmed };
1175
- }
1176
- function validateForm(node, values) {
1177
- if (!isRecord(values.fields)) return { error: "values.fields: expected an object of field ids to strings" };
1178
- const declared = (node.fields ?? []).map((field) => field?.id).filter((id) => typeof id === "string");
1179
- const allowedIds = new Set(declared);
1180
- const fields = {};
1181
- for (const [key, value] of Object.entries(values.fields)) {
1182
- if (!allowedIds.has(key)) return { error: `values.fields.${key}: unknown field; allowed: ${allowed(declared)}` };
1183
- if (typeof value !== "string") return { error: `values.fields.${key}: expected a string` };
1184
- if (value.length > MAX_TEXT) return { error: `values.fields.${key}: ${value.length} characters exceeds the limit of ${MAX_TEXT}` };
1185
- fields[key] = value;
1186
- }
1187
- return { fields };
1188
- }
1189
- //#endregion
1190
- //#region src/answer-text.ts
1191
- function controls(view) {
1192
- return [
1193
- "options",
1194
- "confirm",
1195
- "form",
1196
- "submit"
1197
- ].includes(view.type) ? [view] : (view.children ?? []).flatMap(controls);
1198
- }
1199
- function answerText(card, action, values) {
1200
- const node = controls(card.view).find((node) => node.id === action);
1201
- const lines = [card.title || "Deep View"];
1202
- if (Array.isArray(values.selected)) lines.push(values.selected.map(String).join(", "));
1203
- if (typeof values.confirmed === "boolean") lines.push(values.confirmed ? node?.confirmLabel ?? "Confirm" : node?.cancelLabel ?? "Cancel");
1204
- if (isRecord$1(values.fields)) for (const field of node?.fields ?? []) lines.push(`${field.label}: ${String(values.fields[field.id] ?? "")}`);
1205
- const inputs = inputsSummary(card.view, values.inputs);
1206
- if (inputs) lines.push(inputs);
1207
- if (node?.type === "submit") lines.push(node.label ?? "Submit");
1208
- return lines.join("\n");
1209
- }
1210
- //#endregion
1211
836
  //#region src/index.ts
1212
837
  const name = "deep-view";
1213
838
  const inject = [
@@ -1218,6 +843,40 @@ const inject = [
1218
843
  "connection",
1219
844
  "webServer"
1220
845
  ];
846
+ const MAX_REDIRECTS = 256;
847
+ function resolveSessionId(redirects, sessionId) {
848
+ let current = sessionId;
849
+ const seen = /* @__PURE__ */ new Set();
850
+ while (!seen.has(current)) {
851
+ seen.add(current);
852
+ const next = redirects.get(current);
853
+ if (!next) break;
854
+ current = next;
855
+ }
856
+ return current;
857
+ }
858
+ function handoffPendingSession(pending, redirects, fromSessionId, toSessionId) {
859
+ if (!short(fromSessionId, 200) || !short(toSessionId, 200) || fromSessionId === toSessionId) return false;
860
+ const source = resolveSessionId(redirects, fromSessionId);
861
+ const target = resolveSessionId(redirects, toSessionId);
862
+ if (source === target) return false;
863
+ redirects.set(source, target);
864
+ if (fromSessionId !== source) redirects.set(fromSessionId, target);
865
+ while (redirects.size > MAX_REDIRECTS) {
866
+ const oldest = redirects.keys().next().value;
867
+ if (oldest === void 0) break;
868
+ redirects.delete(oldest);
869
+ }
870
+ const record = pending.get(source);
871
+ if (!record) return false;
872
+ pending.delete(source);
873
+ if (pending.has(target)) return false;
874
+ pending.set(target, {
875
+ ...record,
876
+ sessionId: target
877
+ });
878
+ return true;
879
+ }
1221
880
  const isLiveRoot = (host, agent) => agent !== void 0 && host.agents.roots().some((root) => root.id === agent.id);
1222
881
  const ok = (value) => ({
1223
882
  ok: true,
@@ -1234,9 +893,9 @@ const short = (value, max = 160) => typeof value === "string" && value.length >
1234
893
  const checkedShown = (title, view) => checkShownView(title, view);
1235
894
  const isShown = (value) => !("error" in value);
1236
895
  function presentation(args, value) {
1237
- if (!isRecord$1(value) || value.owner !== "dsh-deep-view" || !["shown", "pending"].includes(String(value.status))) return void 0;
1238
- const shown = checkedShown(isRecord$1(args) ? args.title : void 0, isRecord$1(args) ? args.view : void 0);
1239
- if (!isShown(shown)) return void 0;
896
+ if (!isRecord(value) || value.owner !== "dsh-deep-view" || !["shown", "pending"].includes(String(value.status))) return null;
897
+ const shown = checkedShown(isRecord(args) ? args.title : void 0, isRecord(args) ? args.view : void 0);
898
+ if (!isShown(shown)) return null;
1240
899
  return {
1241
900
  owner: VIEW_OWNER,
1242
901
  ...short(value.viewId, 80) ? { viewId: value.viewId } : {},
@@ -1244,7 +903,7 @@ function presentation(args, value) {
1244
903
  awaitReply: value.awaitReply === true
1245
904
  };
1246
905
  }
1247
- function createShowViewTool(host, pending, makeId = randomUUID) {
906
+ function createShowViewTool(host, pending, makeId = randomUUID, logger) {
1248
907
  return {
1249
908
  name: "show_view",
1250
909
  description: [
@@ -1266,7 +925,7 @@ function createShowViewTool(host, pending, makeId = randomUUID) {
1266
925
  "One ViewNode, nodes nest under stack|row(children,let?).",
1267
926
  "Display: text,metric{label,value},table{columns:[{key,label}],rows:[[cell]]},chart{kind:bar|hbar|line|area|donut,categories,series:[{name?,values}]},progress{percent|value+max},callout{text},sources{items:[{label,url}]}.",
1268
927
  "Inputs: slider,number,choice,toggle{id,label,value}. A {\"=\":\"expr\"} formula replaces a value or cell, reading input ids and let names.",
1269
- "Answers(need await_reply true): options{id,options,multi?},confirm{id},form{id,fields},submit{id,label}. No html."
928
+ "Answers(need await_reply true): options{id,options:[{label,description?}],multi?},confirm{id},form{id,fields:[{id,label,placeholder?}]},submit{id,label}. No html."
1270
929
  ].join(" ")
1271
930
  },
1272
931
  await_reply: {
@@ -1304,14 +963,20 @@ function createShowViewTool(host, pending, makeId = randomUUID) {
1304
963
  presentationMeta: presentation
1305
964
  },
1306
965
  async execute(args, exec) {
1307
- const input = isRecord$1(args) ? args : {};
966
+ const input = isRecord(args) ? args : {};
1308
967
  const shown = checkedShown(input.title, input.view);
1309
- if (!isShown(shown)) return {
1310
- owner: VIEW_OWNER,
1311
- status: "invalid",
1312
- awaitReply: false,
1313
- summary: shown.error
1314
- };
968
+ if (!isShown(shown)) {
969
+ logger?.debug("view.invalid", "Rejected invalid show_view arguments.", {
970
+ sessionId: exec.agent?.id ?? null,
971
+ reason: shown.error
972
+ });
973
+ return {
974
+ owner: VIEW_OWNER,
975
+ status: "invalid",
976
+ awaitReply: false,
977
+ summary: shown.error
978
+ };
979
+ }
1315
980
  if (typeof input.await_reply !== "undefined" && typeof input.await_reply !== "boolean") return {
1316
981
  owner: VIEW_OWNER,
1317
982
  status: "invalid",
@@ -1327,12 +992,15 @@ function createShowViewTool(host, pending, makeId = randomUUID) {
1327
992
  };
1328
993
  const id = exec.agent?.id;
1329
994
  const agent = id === void 0 ? void 0 : host.agents.get(id);
1330
- if (awaitReply && !isLiveRoot(host, agent)) return {
1331
- owner: VIEW_OWNER,
1332
- status: "invalid",
1333
- awaitReply: false,
1334
- summary: "An interactive view only works in a live root session. Put the question or decision in your response instead."
1335
- };
995
+ if (awaitReply && !isLiveRoot(host, agent)) {
996
+ logger?.debug("view.non-root-rejected", "Rejected interactive show_view outside a live root session.", { sessionId: id ?? null });
997
+ return {
998
+ owner: VIEW_OWNER,
999
+ status: "invalid",
1000
+ awaitReply: false,
1001
+ summary: "An interactive view only works in a live root session. Put the question or decision in your response instead."
1002
+ };
1003
+ }
1336
1004
  let viewId;
1337
1005
  if (awaitReply && agent) {
1338
1006
  viewId = makeId();
@@ -1343,6 +1011,11 @@ function createShowViewTool(host, pending, makeId = randomUUID) {
1343
1011
  ...shown
1344
1012
  });
1345
1013
  }
1014
+ logger?.info("view.prepared", "Prepared Deep View card.", {
1015
+ sessionId: agent?.id ?? id ?? null,
1016
+ viewId: viewId ?? null,
1017
+ awaitReply
1018
+ });
1346
1019
  return {
1347
1020
  owner: VIEW_OWNER,
1348
1021
  status: awaitReply ? "pending" : "shown",
@@ -1354,30 +1027,48 @@ function createShowViewTool(host, pending, makeId = randomUUID) {
1354
1027
  isConcurrencySafe: () => false
1355
1028
  };
1356
1029
  }
1357
- function createRpc(host, pending) {
1030
+ function createRpc(host, pending, redirects = /* @__PURE__ */ new Map(), logger) {
1358
1031
  const claims = /* @__PURE__ */ new Set();
1359
1032
  return async (endpoint, payload, signal) => {
1360
- if (!isRecord$1(payload) || !short(payload.sessionId, 200)) return fail("bad-request", "A bounded sessionId is required");
1361
- const sessionId = payload.sessionId;
1033
+ if (!isRecord(payload) || !short(payload.sessionId, 200)) return fail("bad-request", "A bounded sessionId is required");
1034
+ const sessionId = resolveSessionId(redirects, payload.sessionId);
1362
1035
  const record = pending.get(sessionId);
1363
1036
  if (endpoint === "pending") return ok(record?.callId === payload.callId || payload.callId === void 0 ? record ?? null : null);
1364
1037
  if (endpoint === "dismiss") {
1365
- if (record?.id === payload.viewId) pending.delete(sessionId);
1038
+ if (record && record.id === payload.viewId) {
1039
+ pending.delete(sessionId);
1040
+ logger?.debug("view.dismissed", "Dismissed pending Deep View card.", {
1041
+ sessionId,
1042
+ viewId: record.id
1043
+ });
1044
+ }
1366
1045
  return ok(true);
1367
1046
  }
1368
1047
  if (endpoint !== "reply") return fail("not-found", "Unknown Deep View endpoint");
1369
- if (!record || !short(payload.viewId, 80) || payload.viewId !== record.id || !short(payload.action, 160) || !isRecord$1(payload.values)) return fail("stale", "This view is no longer open");
1048
+ if (!record || !short(payload.viewId, 80) || payload.viewId !== record.id || !short(payload.action, 160) || !isRecord(payload.values)) {
1049
+ logger?.debug("reply.stale", "Rejected reply for stale or closed Deep View card.", { sessionId });
1050
+ return fail("stale", "This view is no longer open");
1051
+ }
1370
1052
  if (claims.has(record.id)) return fail("busy", "This view is already being answered");
1371
1053
  const checked = validateViewAnswer(record.view, payload.action, payload.values);
1372
- if (checked.error !== void 0) return fail("invalid", checked.error);
1054
+ if (checked.error !== void 0) {
1055
+ logger?.debug("reply.invalid", "Rejected invalid Deep View reply.", {
1056
+ sessionId,
1057
+ viewId: record.id,
1058
+ action: payload.action,
1059
+ reason: checked.error
1060
+ });
1061
+ return fail("invalid", checked.error);
1062
+ }
1373
1063
  claims.add(record.id);
1374
1064
  pending.delete(sessionId);
1375
1065
  let appended = false;
1376
1066
  try {
1377
1067
  if (signal.aborted) return fail("cancelled", "The request was cancelled");
1378
- const resolved = await host.sessionController.resolveAgent(sessionId);
1068
+ const live = host.agents.get(sessionId);
1069
+ const resolved = isLiveRoot(host, live) ? { agent: live } : await host.sessionController.resolveAgent(sessionId);
1379
1070
  const agent = resolved.agent;
1380
- if (!agent || host.agents.get(agent.id) !== agent || !host.agents.roots().some((root) => root.id === agent.id)) throw new Error(resolved.error?.message ?? "The session is not a live root session");
1071
+ if (!agent || host.agents.get(agent.id) !== agent || !host.agents.roots().some((root) => root.id === agent.id)) throw new Error(("error" in resolved ? resolved.error?.message : void 0) ?? "The session is not a live root session");
1381
1072
  if (signal.aborted) return fail("cancelled", "The request was cancelled");
1382
1073
  const deepView = {
1383
1074
  owner: VIEW_OWNER,
@@ -1400,8 +1091,19 @@ function createRpc(host, pending) {
1400
1091
  }));
1401
1092
  appended = true;
1402
1093
  await host.sessions.flush(agent.session);
1094
+ logger?.info("reply.submitted", "Submitted Deep View reply to live session.", {
1095
+ sessionId: agent.id,
1096
+ viewId: record.id,
1097
+ action: payload.action
1098
+ });
1403
1099
  return ok({ answered: true });
1404
1100
  } catch (error) {
1101
+ logger?.error("reply.failed", "Failed to deliver Deep View reply.", error, {
1102
+ sessionId,
1103
+ viewId: record.id,
1104
+ action: payload.action,
1105
+ appended
1106
+ });
1405
1107
  return fail("internal", error instanceof Error ? error.message : String(error));
1406
1108
  } finally {
1407
1109
  if (!appended && !pending.has(sessionId)) pending.set(sessionId, record);
@@ -1411,26 +1113,41 @@ function createRpc(host, pending) {
1411
1113
  }
1412
1114
  async function apply(ctx) {
1413
1115
  const host = ctx;
1116
+ const logger = installPluginLogger(ctx, PLUGIN_NAME, { forwardHostWarnError: false });
1414
1117
  const pending = /* @__PURE__ */ new Map();
1118
+ const redirects = /* @__PURE__ */ new Map();
1119
+ ctx.provide(HOST_SERVICE, { handoffSession: (fromSessionId, toSessionId) => {
1120
+ const moved = handoffPendingSession(pending, redirects, fromSessionId, toSessionId);
1121
+ if (moved) logger.debug("session.handoff", "Moved pending Deep View card across session handoff.", {
1122
+ fromSessionId,
1123
+ toSessionId
1124
+ });
1125
+ return moved;
1126
+ } });
1415
1127
  ctx.effect(() => {
1416
1128
  if (host.tools.get("show_view")) {
1129
+ logger.warn("tool.already-registered", "show_view is already registered by another plugin; skipping registration.");
1417
1130
  host.logger?.warn("deep-view: show_view is already registered by another plugin; this bundle stays disabled for its own tool");
1418
1131
  return () => {};
1419
1132
  }
1420
1133
  try {
1421
- return host.tools.register(createShowViewTool(host, pending));
1134
+ return host.tools.register(createShowViewTool(host, pending, randomUUID, logger));
1422
1135
  } catch (error) {
1136
+ logger.warn("tool.register-failed", "show_view tool registration failed.", error);
1423
1137
  host.logger?.warn("deep-view: show_view registration skipped: %s", error instanceof Error ? error.message : String(error));
1424
1138
  return () => {};
1425
1139
  }
1426
1140
  }, "deep-view.tool");
1427
1141
  ctx.effect(() => ctx.on("session/event", (session, event) => {
1428
- if (event.type !== "user/message" || !isRecord$1(event.data) || !isRecord$1(event.data.source) || event.data.source.kind !== "user") return;
1429
- pending.delete(session.id);
1142
+ if (event.type !== "user/message" || !isRecord(event.data) || !isRecord(event.data.source) || event.data.source.kind !== "user") return;
1143
+ pending.delete(resolveSessionId(redirects, session.id));
1430
1144
  }), "deep-view.close-pending");
1431
- ctx.effect(() => registerHostRpc(host, RPC_CHANNEL, createRpc(host, pending), { maxBodyBytes: 24576 }), "deep-view.rpc");
1145
+ ctx.effect(() => registerHostRpc(host, RPC_CHANNEL, createRpc(host, pending, redirects, logger), {
1146
+ maxBodyBytes: 24576,
1147
+ logger
1148
+ }), "deep-view.rpc");
1432
1149
  }
1433
1150
  //#endregion
1434
- export { apply, createRpc, createShowViewTool, inject, name };
1151
+ export { apply, createRpc, createShowViewTool, handoffPendingSession, inject, name, resolveSessionId };
1435
1152
 
1436
1153
  //# sourceMappingURL=index.js.map