dsh-context 0.52.1 → 0.52.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -1905,10 +1905,10 @@ window.__ModuleLoader__.load({
1905
1905
  * - INLINE (older hosts, channel-less deployments, the baseline-gate
1906
1906
  * fallback): the wire value carries the collections in place. It passes
1907
1907
  * through untouched — no fetch ever happens.
1908
- * - SPLIT (current host with the detail channel live): the wire value is the
1908
+ * - SPLIT (current host with the detail route live): the wire value is the
1909
1909
  * slim head (~1KB — every session.list row, control baseline, and push
1910
- * frame carries it whole). The collections arrive from the host's
1911
- * `/dsh-context` `detail` endpoint (host/detail.ts): one targeted read
1910
+ * frame carries it whole). The collections arrive from the plugin's
1911
+ * `/api/dsh-context/detail` fetch route (host/detail.ts): one targeted read
1912
1912
  * when the tab/modal first opens, then a debounced refetch whenever the
1913
1913
  * pushed `detailRev` outruns the served detail. Closed tabs fetch nothing.
1914
1914
  *
@@ -1923,8 +1923,7 @@ window.__ModuleLoader__.load({
1923
1923
  * failure surfaces as a retryable note on the detail cards instead of an
1924
1924
  * empty chart.
1925
1925
  */
1926
- const DETAIL_CHANNEL = "/dsh-context";
1927
- const DETAIL_ENDPOINT = "detail";
1926
+ const DETAIL_ROUTE = "/api/dsh-context/detail";
1928
1927
  /**
1929
1928
  * Narrow the detail endpoint's payload to a render-safe value (the same
1930
1929
  * boundary rigor as `timelineOf`): collections re-proved per item, scalars
@@ -1935,8 +1934,10 @@ window.__ModuleLoader__.load({
1935
1934
  const data = asRecord(value);
1936
1935
  if (data === null) return null;
1937
1936
  if (typeof data.rev !== "number" || !Number.isFinite(data.rev) || data.rev < 0) return null;
1937
+ const head = timelineOf(data.head);
1938
1938
  return {
1939
1939
  rev: data.rev,
1940
+ ...head !== null ? { head } : {},
1940
1941
  requests: objectsOf(data.requests),
1941
1942
  events: objectsOf(data.events),
1942
1943
  nodes: objectsOf(data.nodes),
@@ -1949,24 +1950,26 @@ window.__ModuleLoader__.load({
1949
1950
  };
1950
1951
  }
1951
1952
  /**
1952
- * The detail reader over the harness's generic Connection RPC
1953
- * (`ctx.connection.rpc.call`) — resolved through the shared `rpcCallOf`
1954
- * reflect read (the same seam openPathVia/canOpenPathsOf use), so a hostile
1955
- * or absent connection service degrades to `undefined` instead of throwing.
1956
- * The returned thunk resolves the session's current detail, `null` when the
1957
- * session is not live anymore, and rejects on transport failure or a
1958
- * malformed payload (the store turns the two into the retryable state).
1953
+ * The detail reader over the plugin's own `/api` fetch route (host/detail.ts).
1954
+ * Same-origin POST through the harness's authenticated fence; the returned
1955
+ * thunk resolves the session's current detail, `null` when the session is not
1956
+ * live anymore, and rejects on transport failure or a malformed payload (the
1957
+ * store turns the two into the retryable state).
1959
1958
  */
1960
1959
  function makeDetailFetcher(ctx, sessionId) {
1961
1960
  if (sessionId === "") return void 0;
1962
- const call = rpcCallOf(ctx);
1963
- if (call === void 0) return void 0;
1964
1961
  return async () => {
1965
- const r = asRecord(await call(DETAIL_CHANNEL, DETAIL_ENDPOINT, { sessionId }));
1966
- if (r === null || r.ok !== true) throw new Error("dsh-context: detail rpc failed");
1962
+ const response = await fetch(DETAIL_ROUTE, {
1963
+ method: "POST",
1964
+ headers: { "content-type": "application/json" },
1965
+ body: JSON.stringify({ sessionId })
1966
+ });
1967
+ if (!response.ok) throw new Error(`dsh-context: detail route HTTP ${response.status}`);
1968
+ const r = asRecord(await response.json());
1969
+ if (r === null || r.ok !== true) throw new Error("dsh-context: detail read failed");
1967
1970
  if (r.value === null) return null;
1968
1971
  const detail = detailOf(r.value);
1969
- if (detail === null) throw new Error("dsh-context: detail rpc malformed");
1972
+ if (detail === null) throw new Error("dsh-context: detail read malformed");
1970
1973
  return detail;
1971
1974
  };
1972
1975
  }
@@ -6371,8 +6374,15 @@ window.__ModuleLoader__.load({
6371
6374
  * no current session, or a snapshot without a `byId` table (older harness).
6372
6375
  * The current session synthesizes a row when the list has not delivered it
6373
6376
  * yet, so the card can still show its live self stats.
6374
- */
6375
- function agentForestOf(snapshot, currentId, self) {
6377
+ * @param fetched - detail-channel heads for COLD rows, keyed by session id.
6378
+ * The host's list block serves projection values only from the projection
6379
+ * cache, so a relative that never attached since the timeline unit last
6380
+ * changed lists pressure-only (no composition row) and its ring would
6381
+ * degrade to the occupancy arc; the card fetches those heads on demand.
6382
+ * A row's own value always wins — the injection lands only on rows whose
6383
+ * `contextTimeline` is absent.
6384
+ */
6385
+ function agentForestOf(snapshot, currentId, self, fetched) {
6376
6386
  const byId = asRecord(asRecord(snapshot)?.byId);
6377
6387
  if (byId === null || currentId === void 0 || currentId === "") return null;
6378
6388
  const rows = /* @__PURE__ */ new Map();
@@ -6427,7 +6437,12 @@ window.__ModuleLoader__.load({
6427
6437
  const visit = (id, row, parentId, depth, seen, family) => {
6428
6438
  if (seen.has(id) || nodes.length >= 25) return;
6429
6439
  seen.add(id);
6430
- const stats = agentStatsOf(row.projections);
6440
+ const values = row.projections;
6441
+ const fetchedHead = fetched !== void 0 ? fetched.get(id) : void 0;
6442
+ const stats = agentStatsOf(fetchedHead !== void 0 && values?.contextTimeline === void 0 ? {
6443
+ ...values,
6444
+ contextTimeline: fetchedHead
6445
+ } : values);
6431
6446
  const identity = stats.identity;
6432
6447
  const node = {
6433
6448
  ...stats,
@@ -6686,9 +6701,13 @@ window.__ModuleLoader__.load({
6686
6701
  *
6687
6702
  * Data rides the harness's existing planes end to end — the session-list
6688
6703
  * snapshot (`ctx.sessions.list`: lineage rows + per-session projection
6689
- * values) and the tab's own projections for the current node — so the card
6690
- * adds no RPC of its own beyond one direct-child catalog refresh per
6691
- * session. A harness without the outward sessions service hides the card.
6704
+ * values) and the tab's own projections for the current node. The list block
6705
+ * serves projection values only from the host's projection cache, so a
6706
+ * relative that never attached since the timeline unit last changed lists
6707
+ * pressure-only (occupancy without composition); those nodes fetch their slim
6708
+ * head from the plugin's `/api` detail route (the one call below) and
6709
+ * re-render composed. A harness without the outward sessions service hides
6710
+ * the card.
6692
6711
  */
6693
6712
  /** Caption box height under a node (3 wrapped label lines + the tokens line). */
6694
6713
  const CAPTION_H = 60;
@@ -6701,6 +6720,7 @@ window.__ModuleLoader__.load({
6701
6720
  }
6702
6721
  function makeAgentGraph(ctx, kit) {
6703
6722
  const { t, fmt, catLabel } = kit;
6723
+ const heads = /* @__PURE__ */ new Map();
6704
6724
  function AgentGraph(props) {
6705
6725
  const face = (0, react.useMemo)(() => sessionsFaceOf(ctx), []);
6706
6726
  const subscribe = (0, react.useCallback)((fn) => {
@@ -6746,8 +6766,9 @@ window.__ModuleLoader__.load({
6746
6766
  if (typeof face.refreshSubagents !== "function") return;
6747
6767
  face.refreshSubagents(sessionId).catch(() => {});
6748
6768
  }, [face, sessionId]);
6769
+ const [landed, setLanded] = (0, react.useState)(/* @__PURE__ */ new Map());
6749
6770
  const built = (0, react.useMemo)(() => {
6750
- const forest = agentForestOf(snapshot, sessionId, props.self);
6771
+ const forest = agentForestOf(snapshot, sessionId, props.self, landed);
6751
6772
  return forest !== null ? {
6752
6773
  forest,
6753
6774
  layout: layoutForest(forest, stageWidth)
@@ -6756,8 +6777,29 @@ window.__ModuleLoader__.load({
6756
6777
  snapshot,
6757
6778
  sessionId,
6758
6779
  props.self,
6759
- stageWidth
6780
+ stageWidth,
6781
+ landed
6760
6782
  ]);
6783
+ (0, react.useEffect)(() => {
6784
+ if (built === null) return;
6785
+ const attach = (pending, id) => {
6786
+ pending.then((head) => {
6787
+ if (head !== null) setLanded((prev) => prev.get(id) === head ? prev : new Map(prev).set(id, head));
6788
+ }).catch(() => {});
6789
+ };
6790
+ for (const n of built.forest.nodes) {
6791
+ if (n.isCurrent || n.head !== null && n.head.parts.length > 0) continue;
6792
+ const cached = heads.get(n.id);
6793
+ if (cached !== void 0) {
6794
+ attach(cached, n.id);
6795
+ continue;
6796
+ }
6797
+ const fetcher = makeDetailFetcher(ctx, n.id);
6798
+ const pending = fetcher !== void 0 ? fetcher().then((d) => d?.head ?? null) : Promise.resolve(null);
6799
+ heads.set(n.id, pending);
6800
+ attach(pending, n.id);
6801
+ }
6802
+ }, [built]);
6761
6803
  if (built === null) return null;
6762
6804
  const { forest, layout } = built;
6763
6805
  const byId = new Map(forest.nodes.map((n) => [n.id, n]));
@@ -7599,7 +7641,7 @@ window.__ModuleLoader__.load({
7599
7641
  return function PluginInfo() {
7600
7642
  const [latest, setLatest] = (0, react.useState)(null);
7601
7643
  (0, react.useEffect)(() => {
7602
- if ("0.52.1".includes("-dev")) return;
7644
+ if ("0.52.2".includes("-dev")) return;
7603
7645
  let on = true;
7604
7646
  fetchLatestVersion().then((v) => {
7605
7647
  if (on && v) setLatest(v);
@@ -7608,8 +7650,8 @@ window.__ModuleLoader__.load({
7608
7650
  on = false;
7609
7651
  };
7610
7652
  }, []);
7611
- const update = latest !== null && isNewerVersion(latest, "0.52.1") ? latest : null;
7612
- const nameText = "dsh-context (v0.52.1)";
7653
+ const update = latest !== null && isNewerVersion(latest, "0.52.2") ? latest : null;
7654
+ const nameText = "dsh-context (v0.52.2)";
7613
7655
  const nameValue = [nameText];
7614
7656
  if (update) nameValue.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7615
7657
  className: "lc-pi-update",
package/lib/index.d.ts CHANGED
@@ -446,6 +446,17 @@ interface Snapshot {
446
446
  */
447
447
  interface ContextTimelineDetail {
448
448
  rev: number;
449
+ /**
450
+ * The slim wire head at the SAME fold cut as the collections: the
451
+ * composition scalars (`current`), the window/model envelope, and the
452
+ * precomputed counts. Sessions listed cold (never attached since the
453
+ * requesting unit last changed) carry no `contextTimeline` projection row
454
+ * for the browser's list reads, so the Agent network card fetches this
455
+ * head per node to render their composition rings. The host always serves
456
+ * it; optional so a payload missing it still serves the collections (the
457
+ * detail cards) and only the ring composition degrades.
458
+ */
459
+ head?: ContextTimeline;
449
460
  requests: RequestRecord[];
450
461
  events: ContextEventRecord[];
451
462
  nodes: SurfaceNode[];
package/lib/index.js CHANGED
@@ -1933,11 +1933,14 @@ function buildTimelineHead(state) {
1933
1933
  }
1934
1934
  /**
1935
1935
  * The on-demand detail payload (host/detail.ts serves it off the live fold
1936
- * state): the heavy collections plus the revision marker the head carries.
1936
+ * state): the heavy collections, the slim head at the SAME cut (the Agent
1937
+ * network's cold-node ring fetch renders composition off it), and the
1938
+ * revision marker the head carries.
1937
1939
  */
1938
1940
  function buildTimelineDetail(state, bounds) {
1939
1941
  return {
1940
1942
  rev: state.detailRev ?? 0,
1943
+ head: buildTimelineHead(state),
1941
1944
  ...detailCollectionsOf(state, bounds)
1942
1945
  };
1943
1946
  }
@@ -1956,63 +1959,71 @@ function buildTimelineView(state, bounds) {
1956
1959
  }
1957
1960
  //#endregion
1958
1961
  //#region src/host/detail.ts
1959
- /** The plugin's generic Connection RPC channel (the pre-v0.9 name, kept). */
1960
- const DETAIL_CHANNEL = "/dsh-context";
1961
- /** The RPC failure envelope the transport expects (ConnectionRpcFailure). */
1962
+ /** The plugin's detail route, under the authenticated `/api` fence. */
1963
+ const DETAIL_ROUTE = "/api/dsh-context/detail";
1964
+ /** One JSON reply of the detail route. */
1965
+ function reply(value) {
1966
+ return Response.json(value, { headers: { "cache-control": "no-store" } });
1967
+ }
1968
+ /** The typed failure envelope (the same shape the Connection RPC carried). */
1962
1969
  function failure(code, message) {
1963
- return {
1970
+ return reply({
1964
1971
  ok: false,
1965
1972
  error: {
1966
1973
  code,
1967
- message,
1968
- details: {}
1974
+ message
1969
1975
  }
1970
- };
1976
+ });
1971
1977
  }
1972
1978
  /**
1973
- * Arm the detail endpoint whenever the connection and sessions services are
1979
+ * Serve the detail route whenever the connection and sessions services are
1974
1980
  * both composed (see the module header for the load-order contract). The
1975
1981
  * registration rides the injected fiber: either service unloading withdraws
1976
- * the channel and closes the gate.
1982
+ * the route and closes the gate.
1977
1983
  */
1978
1984
  function watchDetailChannel(ctx, bounds) {
1979
1985
  const gate = { live: false };
1980
1986
  ctx.inject(["connection", "sessions"], (c) => {
1981
1987
  const connection = c.get("connection");
1982
1988
  const sessions = c.get("sessions");
1983
- const handle = typeof connection?.rpc?.handle === "function" ? connection.rpc.handle.bind(connection.rpc) : void 0;
1989
+ const register = typeof connection?.fetch?.register === "function" ? connection.fetch.register.bind(connection.fetch) : void 0;
1984
1990
  const getSession = typeof sessions?.get === "function" ? sessions.get.bind(sessions) : void 0;
1985
- if (handle === void 0 || getSession === void 0) return;
1991
+ if (register === void 0 || getSession === void 0) return;
1986
1992
  const projections = ctx.sessionProjections;
1987
- const handler = async (endpoint, payload) => {
1988
- if (endpoint !== "detail") return failure("dsh-context/unknown-endpoint", `unknown endpoint: ${endpoint}`);
1989
- const sessionId = payload !== null && typeof payload === "object" ? payload.sessionId : void 0;
1993
+ const handler = async (request) => {
1994
+ let sessionId;
1995
+ try {
1996
+ const body = await request.json();
1997
+ sessionId = body !== null && typeof body === "object" ? body.sessionId : void 0;
1998
+ } catch {
1999
+ return failure("dsh-context/bad-request", "body is not JSON");
2000
+ }
1990
2001
  if (typeof sessionId !== "string" || sessionId === "") return failure("dsh-context/bad-request", "missing sessionId");
1991
2002
  try {
1992
2003
  const session = getSession(sessionId);
1993
2004
  if (session !== void 0 && session !== null) {
1994
2005
  const state = projections.stateOf(session, "contextTimeline");
1995
- if (state === void 0) return {
2006
+ if (state === void 0) return reply({
1996
2007
  ok: true,
1997
2008
  value: null
1998
- };
1999
- return {
2009
+ });
2010
+ return reply({
2000
2011
  ok: true,
2001
2012
  value: buildTimelineDetail(state, bounds)
2002
- };
2013
+ });
2003
2014
  }
2004
2015
  const query = ctx.get("sessionQuery");
2005
2016
  const observe = typeof query?.observeSession === "function" ? query.observeSession.bind(query) : void 0;
2006
- if (observe === void 0) return {
2017
+ if (observe === void 0) return reply({
2007
2018
  ok: true,
2008
2019
  value: null
2009
- };
2020
+ });
2010
2021
  const observation = await observe(sessionId, { projectionMode: "none" });
2011
2022
  const events = observation?.events;
2012
- if (!Array.isArray(events)) return {
2023
+ if (!Array.isArray(events)) return reply({
2013
2024
  ok: true,
2014
2025
  value: null
2015
- };
2026
+ });
2016
2027
  let state = createTimelineState();
2017
2028
  try {
2018
2029
  for (const ev of events) state = applyTimeline(state, ev, bounds);
@@ -2020,21 +2031,21 @@ function watchDetailChannel(ctx, bounds) {
2020
2031
  const dispose = observation?.[Symbol.dispose];
2021
2032
  if (typeof dispose === "function") dispose.call(observation);
2022
2033
  }
2023
- return {
2034
+ return reply({
2024
2035
  ok: true,
2025
2036
  value: buildTimelineDetail(state, bounds)
2026
- };
2037
+ });
2027
2038
  } catch (err) {
2028
2039
  return failure("gateway/internal", err instanceof Error ? err.message : String(err));
2029
2040
  }
2030
2041
  };
2031
2042
  try {
2032
- c.effect(() => {
2033
- const unregister = handle(DETAIL_CHANNEL, handler);
2034
- return () => {
2035
- unregister();
2036
- };
2037
- }, "dsh-context: detail channel");
2043
+ c.effect(() => register({
2044
+ path: DETAIL_ROUTE,
2045
+ methods: ["POST"],
2046
+ requestBody: "buffered",
2047
+ fetch: handler
2048
+ }), "dsh-context: detail route");
2038
2049
  } catch {
2039
2050
  return;
2040
2051
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-context",
3
- "version": "0.52.1",
3
+ "version": "0.52.2",
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": {