dsh-context 0.44.0 → 0.45.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.
Files changed (3) hide show
  1. package/lib/client.js +2613 -1255
  2. package/lib/index.js +80 -0
  3. package/package.json +1 -1
package/lib/index.js CHANGED
@@ -5,6 +5,7 @@ import { fileURLToPath } from "node:url";
5
5
  import { z } from "zod";
6
6
  import { deriveEventMessage } from "@deepseek-ai/dsh-session";
7
7
  import z$1 from "@deepseek-ai/schemastery";
8
+ import { randomUUID } from "node:crypto";
8
9
  /**
9
10
  * Recover the MCP server display label from a proxied tool name, or undefined
10
11
  * for non-MCP names. `dsh-mcp-client` names tools `mcp__<server>__<rawName>`
@@ -2305,6 +2306,84 @@ function installSettings(ctx) {
2305
2306
  });
2306
2307
  }
2307
2308
  //#endregion
2309
+ //#region src/host/stepIdentity.ts
2310
+ /**
2311
+ * Step-boundary message identity guard (issue #51 compatibility).
2312
+ *
2313
+ * The harness refuses to LOAD any session whose durable log carries a
2314
+ * `user/message` event whose message lacks a non-empty string `id`
2315
+ * (`assertMessageEventShape`) — while the runtime append path runs no such
2316
+ * check. One unidentified message therefore persists silently and permanently
2317
+ * bricks the session at its next load (`... failed validation: session event
2318
+ * at seq N lacks an identified message`). The observed archive held a wrap-up
2319
+ * notice whose `id` (and `summary`) had been stripped upstream of the append —
2320
+ * a shape no shipped harness producer emits, i.e. a delivery-boundary rebuild
2321
+ * outside the audited paths of both this plugin and the harness.
2322
+ *
2323
+ * This guard hardens the durability boundary from the plugin side. The agent
2324
+ * loop appends every step-boundary `user/message` from the `agent/pre-step`
2325
+ * decision's `messages` — the one seam all claimed inbox input (prompts,
2326
+ * steering, tool-deferred context) flows through before it persists. A
2327
+ * prepended listener sits OUTERMOST in that waterfall, so after `next()`
2328
+ * resolves it sees the final message list: any message that would persist
2329
+ * unidentified gets a fresh id; everything else passes by reference, and the
2330
+ * decision object is copied only when a mint happened.
2331
+ *
2332
+ * Fail-open by contract: a hostile entry (one that throws on property access),
2333
+ * a missing list, or any unexpected shape leaves the decision verbatim — a
2334
+ * guard must never break the turn it protects. Minted ids carry the `dshctx-`
2335
+ * prefix so a backfilled message stays identifiable in the wild.
2336
+ */
2337
+ /** Prefix marking an id this guard minted (forensically distinguishable). */
2338
+ const MINTED_ID_PREFIX = "dshctx-";
2339
+ /**
2340
+ * Whether the message fails the harness's restore-time identity check (a
2341
+ * non-empty string id). Non-object entries are unfixable (an id needs a
2342
+ * container) and stay verbatim.
2343
+ */
2344
+ function lacksId(message) {
2345
+ if (typeof message !== "object" || message === null) return false;
2346
+ const id = message.id;
2347
+ return typeof id !== "string" || id === "";
2348
+ }
2349
+ /**
2350
+ * Mint ids for the messages that would persist unidentified.
2351
+ * @param messages - the decision's message list, in append order.
2352
+ * @returns the rewritten list (untouched entries by reference), or undefined
2353
+ * when every entry already carries an identity.
2354
+ */
2355
+ function identifiedMessages(messages) {
2356
+ let copy;
2357
+ for (const [index, message] of messages.entries()) {
2358
+ if (!lacksId(message)) {
2359
+ copy?.push(message);
2360
+ continue;
2361
+ }
2362
+ copy ??= messages.slice(0, index);
2363
+ copy.push({
2364
+ ...message,
2365
+ id: MINTED_ID_PREFIX + randomUUID()
2366
+ });
2367
+ }
2368
+ return copy;
2369
+ }
2370
+ /** Arm the prepended `agent/pre-step` guard on the plugin's context. */
2371
+ function watchStepIdentity(ctx) {
2372
+ ctx.on("agent/pre-step", async (_input, next) => {
2373
+ const decision = await next();
2374
+ try {
2375
+ if (decision.kind !== "enter" || !Array.isArray(decision.messages)) return decision;
2376
+ const identified = identifiedMessages(decision.messages);
2377
+ return identified === void 0 ? decision : {
2378
+ ...decision,
2379
+ messages: identified
2380
+ };
2381
+ } catch {
2382
+ return decision;
2383
+ }
2384
+ }, { prepend: true });
2385
+ }
2386
+ //#endregion
2308
2387
  //#region src/host/version.ts
2309
2388
  /**
2310
2389
  * Runtime harness-version probe behind the baseline gate (host/index.ts).
@@ -2422,6 +2501,7 @@ function apply(ctx, config) {
2422
2501
  return;
2423
2502
  }
2424
2503
  const attribution = createToolAttribution(ctx);
2504
+ watchStepIdentity(ctx);
2425
2505
  const gate = watchDetailChannel(ctx, resolveBounds(config));
2426
2506
  ctx.sessionProjections.register(createContextTimelineDefinition(config, () => gate.live));
2427
2507
  ctx.sessionProjections.register(createContextHeadersDefinition((name) => attribution.ownerOf(name)));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-context",
3
- "version": "0.44.0",
3
+ "version": "0.45.0",
4
4
  "description": "A DeepSeek Harness plugin for context insight and management, with context dashboard and context command, for understanding how the context is made of, and how it evolves.",
5
5
  "author": "bowenliang123",
6
6
  "repository": {