dsh-hypatia-auto-memory 2.0.0 → 2.1.1
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/README.md +37 -3
- package/README.zh-CN.md +28 -4
- package/lib/client.js +199 -7
- package/lib/client.js.map +1 -1
- package/package.json +4 -3
- package/skills/hypatia-memory/SKILL.md +47 -3
- package/src/client/MemoryView.module.css +38 -1
- package/src/client/MemoryView.tsx +94 -1
- package/src/client/index.tsx +2 -2
- package/src/client/locales.ts +22 -0
- package/src/client/memory-client.ts +96 -0
- package/src/consolidator.js +115 -29
- package/src/index.js +56 -3
- package/src/memory-api.js +81 -7
- package/src/on-demand.js +249 -0
package/README.md
CHANGED
|
@@ -101,6 +101,8 @@ collect fiber dispose ──▶ the same flush for every session still open, per
|
|
|
101
101
|
before the queue stops; the model call finishes at the next start
|
|
102
102
|
(a restart, a config-change restart, or a crash: the startup
|
|
103
103
|
consolidation backfill)
|
|
104
|
+
explicit request ──▶ /hypatia-summarize (no model turn) | hypatia_summarize |
|
|
105
|
+
the Memory tab button ──▶ the same consolidate task, thresholds waived
|
|
104
106
|
agent/created ──▶ rules/taboos inject()
|
|
105
107
|
```
|
|
106
108
|
|
|
@@ -221,6 +223,34 @@ agent/created ──▶ rules/taboos inject()
|
|
|
221
223
|
This is the protocol's log₁₆(n) archive: each tier compresses only material a
|
|
222
224
|
previous tier already distilled.
|
|
223
225
|
|
|
226
|
+
- **Consolidation can also be requested explicitly.** Three faces call one host
|
|
227
|
+
action: the slash command **`/hypatia-summarize`** (in the composer's `/` menu,
|
|
228
|
+
executed by the UI *without a model turn*), the **`hypatia_summarize`** tool
|
|
229
|
+
(the `hypatia-memory` skill's execution path, so the agent never hand-writes a
|
|
230
|
+
`sum-*` entry), and a **Summarise now** button on the Memory tab, which sits on
|
|
231
|
+
the row that reports how far behind the session is — the moment a reader wants
|
|
232
|
+
it. A request cuts the span `[lastConsolidatedSeq, min(seq, lastLoggedSeq))`
|
|
233
|
+
**at that moment** and queues the same `consolidate` task the automatic trigger
|
|
234
|
+
queues: same kind, same executor, same idempotent writer. `checkEveryTurns` and
|
|
235
|
+
`minNewTokens` are deliberately bypassed — they pace an ongoing conversation,
|
|
236
|
+
and an explicit request is the reason to cut now. Measured live, a session sat
|
|
237
|
+
151 logged events behind while the token floor had accumulated 645 of 3000, so
|
|
238
|
+
waiting for the gate is not an answer to accept. The config switches are *not*
|
|
239
|
+
bypassed, and a request is only a cut, never a freeze: events appended
|
|
240
|
+
afterwards wait for the next trigger as usual.
|
|
241
|
+
|
|
242
|
+
"Nothing left to summarise" and "this span is already queued" are reported as
|
|
243
|
+
such rather than queued again, so asking twice does not produce two summaries —
|
|
244
|
+
and because the writer is idempotent by name, even a race cannot. The button is
|
|
245
|
+
**disabled but kept in place** once the session is caught up: hiding it would
|
|
246
|
+
make the user rediscover it every time the session falls behind, which is
|
|
247
|
+
exactly when they look for it. It rides the Memory tab's own route family,
|
|
248
|
+
whose first **write** route (`POST /consolidate`) keeps all three
|
|
249
|
+
self-authentication checks the read routes use — loopback socket, loopback
|
|
250
|
+
`Host`, same-origin — and refuses anything but `POST`, so the write cannot be
|
|
251
|
+
triggered by a link or an image tag. Nothing on any of the three faces runs a
|
|
252
|
+
model call in the main session.
|
|
253
|
+
|
|
224
254
|
- **Work units** are adjudicated, not guessed. Candidates come from `similar`
|
|
225
255
|
(the only search that reports a distance), with the operational layer left
|
|
226
256
|
out — in the query, by `--exclude-tags`, on a binary that has it (hypatia
|
|
@@ -325,7 +355,7 @@ agent/created ──▶ rules/taboos inject()
|
|
|
325
355
|
verbatim, the reader just wrote them, and the agent is the right reader for
|
|
326
356
|
them.
|
|
327
357
|
|
|
328
|
-
The data arrives over a session-scoped
|
|
358
|
+
The data arrives over a session-scoped HTTP route family this plugin
|
|
329
359
|
registers on the DSH web server, `/api/dsh-hypatia-auto-memory/session`: status
|
|
330
360
|
from the in-memory state tables on every poll, the shelf content only when it
|
|
331
361
|
can have changed (on open, on a session switch, on a manual refresh, and when
|
|
@@ -335,7 +365,10 @@ agent/created ──▶ rules/taboos inject()
|
|
|
335
365
|
and 120 000 per response. JSE has no `ORDER BY`, so "newest" is decided only
|
|
336
366
|
after reading every candidate; when a scan reaches its cap the counts become
|
|
337
367
|
lower bounds, and the tab reports that response as truncated rather than
|
|
338
|
-
leaving a short list unexplained.
|
|
368
|
+
leaving a short list unexplained. The family's one **write** route,
|
|
369
|
+
`POST /consolidate`, is what the **Summarise now** button calls; it writes
|
|
370
|
+
nothing itself — it queues the same consolidation task the automatic trigger
|
|
371
|
+
queues (see *On-demand* above).
|
|
339
372
|
Two channels were rejected on the way here. A **session projection** would be
|
|
340
373
|
the best shape, but the event that would drive it cannot be written from
|
|
341
374
|
outside this repository: `Session.append` offers no way to set the envelope's
|
|
@@ -650,7 +683,8 @@ dsh-hypatia-auto-memory/
|
|
|
650
683
|
│ ├── cascade.js # log₁₆(n) hierarchical summary archive
|
|
651
684
|
│ ├── model-log.js # bounded record of which model each attempt ran on
|
|
652
685
|
│ ├── memory-status.js # per-session fold of the two state tables (pure)
|
|
653
|
-
│ ├── memory-api.js # Memory tab + shelf listing
|
|
686
|
+
│ ├── memory-api.js # Memory tab + shelf listing; its one write route too
|
|
687
|
+
│ ├── on-demand.js # /hypatia-summarize + the hypatia_summarize tool
|
|
654
688
|
│ ├── recall.js # rules/taboos preload at session start
|
|
655
689
|
│ ├── auto-approve.js # approves the agent's own plain bash hypatia calls
|
|
656
690
|
│ ├── skills.js # bundled skill registration (never shadows another provider)
|
package/README.zh-CN.md
CHANGED
|
@@ -87,6 +87,8 @@ session/disposed ──▶ final flush + consolidation, thresholds waived (DSH
|
|
|
87
87
|
collect fiber 拆除 ──▶ 对每个仍打开的会话做同样的收尾,并在队列停止前落盘;模型
|
|
88
88
|
调用留到下次启动完成(重启、配置变更重启,或崩溃:启动时的
|
|
89
89
|
consolidation backfill)
|
|
90
|
+
显式请求 ──▶ /hypatia-summarize(不过模型)| hypatia_summarize | 记忆标签页按钮
|
|
91
|
+
──▶ 同一个 consolidate 任务,但跳过阈值
|
|
90
92
|
agent/created ──▶ rules/taboos inject()
|
|
91
93
|
```
|
|
92
94
|
|
|
@@ -175,6 +177,25 @@ agent/created ──▶ rules/taboos inject()
|
|
|
175
177
|
免费得到 FIFO 分批)。这就是协议的 log₁₆(n) 归档:每一层只压缩上一层已经蒸馏过的
|
|
176
178
|
内容。
|
|
177
179
|
|
|
180
|
+
- **也可以显式要求立刻整合。** 三个面对应同一个 host 侧动作:斜杠命令
|
|
181
|
+
**`/hypatia-summarize`**(在 composer 的 `/` 菜单里,由 UI 执行、**不过模型**)、工具
|
|
182
|
+
**`hypatia_summarize`**(`hypatia-memory` 技能的执行路径,Agent 因此不会自己手写
|
|
183
|
+
`sum-*` 条目),以及记忆标签页上的**「立即整理」按钮**——它就在报告「落后多少」的那
|
|
184
|
+
一行上,正是读者需要它的时刻。一次请求按**触发那一刻**的水位切出
|
|
185
|
+
`[lastConsolidatedSeq, min(seq, lastLoggedSeq))`,入队与自动触发**完全相同**的
|
|
186
|
+
`consolidate` 任务:同一种 kind、同一个执行器、同一个幂等写手。`checkEveryTurns` 与
|
|
187
|
+
`minNewTokens` 被刻意绕过——它们用来给进行中的对话定节奏,而显式请求本身就是「现在
|
|
188
|
+
就切」的理由。实测中一个会话落后 151 个事件,而门槛只累计到 3000 中的 645,所以
|
|
189
|
+
「等闸门开」不是一个应当被接受的答案。配置开关**不**绕过;而且请求只是一次切分,
|
|
190
|
+
不是冻结:之后产生的事件照旧等下一次触发。
|
|
191
|
+
|
|
192
|
+
「没有需要整理的内容」和「这个区间已经入队」会如实回报而不是再入队一次,所以问两遍
|
|
193
|
+
不会产生两条摘要——何况写手按名字幂等,即使并发也不会。结清后按钮**禁用但保留在原
|
|
194
|
+
位**:隐藏它会让用户每次落后时都要重新发现它,而那正是他们找它的时候。它走记忆标签
|
|
195
|
+
页自己的路由族,其中第一条**写**路由(`POST /consolidate`)完整保留读路由的三条自
|
|
196
|
+
认证——回环 socket、回环 `Host`、同源——并且只接受 `POST`,所以链接或 `<img>` 无法触
|
|
197
|
+
发这次写入。三个面都不会在主会话里发起模型调用。
|
|
198
|
+
|
|
178
199
|
- **工作单元是裁决出来的,不是猜出来的。** 候选来自 `similar`(唯一会报告距离的
|
|
179
200
|
检索),且排除操作层——在具备该能力的二进制上(hypatia #35)通过查询里的
|
|
180
201
|
`--exclude-tags`;在没这个能力的二进制上,抓四倍的量再丢弃——再按距离上限过滤,
|
|
@@ -250,14 +271,16 @@ agent/created ──▶ rules/taboos inject()
|
|
|
250
271
|
原始 `msg-*` 条目**故意不显示**:它是对话原文,读的人刚刚写过,而正确的读者是
|
|
251
272
|
Agent。
|
|
252
273
|
|
|
253
|
-
数据经由本插件注册在 DSH web
|
|
274
|
+
数据经由本插件注册在 DSH web 服务器上的、会话作用域的 HTTP 路由族
|
|
254
275
|
`/api/dsh-hypatia-auto-memory/session` 到达浏览器:状态每次轮询都从内存中的状态表
|
|
255
276
|
现算,shelf 内容只在可能变化时才读(打开时、切换会话时、手动刷新时,以及整合推进了
|
|
256
277
|
该会话水位时),其间缓存 15 秒。标签页在屏幕上时每 5 秒轮询一次,响应是有界的:
|
|
257
278
|
最新的 40 条摘要与 40 条工作单元,每条正文 4000 字符、整份响应 120000 字符。
|
|
258
279
|
JSE 没有 `ORDER BY`,所以「最新」只能在读完所有候选之后判定;扫描触到上限时计数
|
|
259
|
-
|
|
260
|
-
|
|
280
|
+
变成下界,标签页会把该次响应标为已截断,而不是让短列表无从解释。这个路由族里唯一的
|
|
281
|
+
**写**路由 `POST /consolidate` 就是「立即整理」按钮调用的那个;它自己不写任何东西
|
|
282
|
+
——它入队的是与自动触发完全相同的整合任务(见上文「也可以显式要求立刻整合」)。
|
|
283
|
+
路上否掉了两条通道。**session projection** 形态最
|
|
261
284
|
合适,但驱动它的事件在本仓库之外写不出来:`Session.append` 没有任何途径设置信封的
|
|
262
285
|
`ignorable` 标记,而持久化读取路径会拒绝一个带着未知事件类型、又没有该标记的会话
|
|
263
286
|
日志——所以自定义事件会破坏会话重载。**设置命名空间**即便在还存在时也是根作用域的:
|
|
@@ -513,7 +536,8 @@ dsh-hypatia-auto-memory/
|
|
|
513
536
|
│ ├── cascade.js # log₁₆(n) 分层摘要归档
|
|
514
537
|
│ ├── model-log.js # 每次尝试实际用了哪个模型的有界记录
|
|
515
538
|
│ ├── memory-status.js # 两张状态表按会话折叠(纯函数)
|
|
516
|
-
│ ├── memory-api.js # 记忆标签页与 shelf
|
|
539
|
+
│ ├── memory-api.js # 记忆标签页与 shelf 清单;含唯一的写路由
|
|
540
|
+
│ ├── on-demand.js # /hypatia-summarize 与 hypatia_summarize 工具
|
|
517
541
|
│ ├── recall.js # 会话启动时的 rules/taboos 预载
|
|
518
542
|
│ ├── auto-approve.js # 批准 Agent 自己的纯 bash hypatia 调用
|
|
519
543
|
│ ├── skills.js # 随包技能注册(从不遮蔽其它提供者)
|
package/lib/client.js
CHANGED
|
@@ -307,6 +307,17 @@ window.__ModuleLoader__.load({
|
|
|
307
307
|
memoryConsolidated: "摘要整合",
|
|
308
308
|
memoryCaughtUp: "已结清",
|
|
309
309
|
memoryBehind: "较记录落后 {behind} 条",
|
|
310
|
+
memorySummarize: "立即整理",
|
|
311
|
+
memorySummarizing: "整理中…",
|
|
312
|
+
memorySummarizeHint: "不等阈值,立刻把这段尚未摘要的对话交给后台整合。已结清时按钮禁用但保留在原位,下次落后时不必重新找它。",
|
|
313
|
+
memorySummarizeQueued: "已入队:将整理第 {from} – {to} 条事件;摘要在后台由插件的专用模型路由写入,不会占用本会话的上下文。",
|
|
314
|
+
memorySummarizeEmpty: "已结清,没有需要整理的内容。",
|
|
315
|
+
memorySummarizeBusy: "同样的区间已有一次整合在队列中,无需重复触发。",
|
|
316
|
+
memorySummarizeDisabled: "整合功能已在设置中关闭。",
|
|
317
|
+
memorySummarizeUnavailable: "整合不可用:当前组合没有挂载模型路由。",
|
|
318
|
+
memorySummarizeUnknown: "这个会话已经不在内存中,无法整理。",
|
|
319
|
+
memorySummarizeUnknownReason: "未能整理:{reason}",
|
|
320
|
+
memorySummarizeFailed: "无法触发整理:{message}",
|
|
310
321
|
memoryPending: "待整合内容",
|
|
311
322
|
memoryPendingValue: "约 {tokens} token",
|
|
312
323
|
memorySessionNode: "会话节点",
|
|
@@ -407,6 +418,17 @@ window.__ModuleLoader__.load({
|
|
|
407
418
|
memoryConsolidated: "Summarisation",
|
|
408
419
|
memoryCaughtUp: "caught up",
|
|
409
420
|
memoryBehind: "{behind} events behind the log",
|
|
421
|
+
memorySummarize: "Summarise now",
|
|
422
|
+
memorySummarizing: "Summarising…",
|
|
423
|
+
memorySummarizeHint: "Consolidate the part of this conversation that has not been summarised yet, without waiting for the thresholds. Once caught up the button is disabled but stays in place, so it need not be rediscovered the next time the session falls behind.",
|
|
424
|
+
memorySummarizeQueued: "Queued: events {from}–{to} will be summarised. The summary is written in the background on the plugin's own model route and costs this session no context.",
|
|
425
|
+
memorySummarizeEmpty: "Nothing to summarise: the log is already consolidated.",
|
|
426
|
+
memorySummarizeBusy: "A consolidation covering the same span is already queued.",
|
|
427
|
+
memorySummarizeDisabled: "Consolidation is turned off in the settings.",
|
|
428
|
+
memorySummarizeUnavailable: "Consolidation is unavailable: this composition mounted no model route for it.",
|
|
429
|
+
memorySummarizeUnknown: "That session is no longer in memory, so it cannot be consolidated.",
|
|
430
|
+
memorySummarizeUnknownReason: "Could not consolidate: {reason}",
|
|
431
|
+
memorySummarizeFailed: "Could not start consolidation: {message}",
|
|
410
432
|
memoryPending: "Waiting to summarise",
|
|
411
433
|
memoryPendingValue: "~{tokens} tokens",
|
|
412
434
|
memorySessionNode: "Session node",
|
|
@@ -1341,6 +1363,22 @@ window.__ModuleLoader__.load({
|
|
|
1341
1363
|
/** Route prefix the Host claims; the tab appends `/session`. */
|
|
1342
1364
|
const MEMORY_API_PREFIX = "/api/dsh-hypatia-auto-memory";
|
|
1343
1365
|
/**
|
|
1366
|
+
* Why an explicit consolidation request queued nothing, matching the Host's
|
|
1367
|
+
* `SUMMARIZE_REASON` vocabulary (src/consolidator.js). Kept as named constants
|
|
1368
|
+
* so the view's switch and the Host's answers cannot drift apart on a typo.
|
|
1369
|
+
*/
|
|
1370
|
+
const SUMMARIZE_REASON = {
|
|
1371
|
+
disabled: "disabled",
|
|
1372
|
+
empty: "empty",
|
|
1373
|
+
busy: "busy",
|
|
1374
|
+
unknownSession: "unknown-session",
|
|
1375
|
+
unavailable: "unavailable"
|
|
1376
|
+
};
|
|
1377
|
+
/** Whether "summarise now" has work to do for this session. */
|
|
1378
|
+
function canSummarize(session) {
|
|
1379
|
+
return session.known && !session.caughtUp;
|
|
1380
|
+
}
|
|
1381
|
+
/**
|
|
1344
1382
|
* The URL one refresh reads.
|
|
1345
1383
|
* @param sessionId - the session the tab is bound to.
|
|
1346
1384
|
* @param content - whether to ask the Host to read the shelf as well.
|
|
@@ -1368,6 +1406,59 @@ window.__ModuleLoader__.load({
|
|
|
1368
1406
|
return payload;
|
|
1369
1407
|
};
|
|
1370
1408
|
/**
|
|
1409
|
+
* The URL one explicit consolidation request POSTs to.
|
|
1410
|
+
* @param sessionId - the session to consolidate.
|
|
1411
|
+
* @returns a same-origin path with its query.
|
|
1412
|
+
*/
|
|
1413
|
+
function summarizeUrl(sessionId) {
|
|
1414
|
+
const query = new URLSearchParams({ session: sessionId });
|
|
1415
|
+
return `${MEMORY_API_PREFIX}/consolidate?${query.toString()}`;
|
|
1416
|
+
}
|
|
1417
|
+
/**
|
|
1418
|
+
* Ask the Host to consolidate one session now.
|
|
1419
|
+
*
|
|
1420
|
+
* `POST`, never `GET`: a write must not be reachable from a link or an image
|
|
1421
|
+
* tag. The Host keeps the same self-authentication on this route as on the read
|
|
1422
|
+
* ones (loopback socket, loopback `Host`, same-origin), and the body is empty —
|
|
1423
|
+
* everything the request means is in the URL.
|
|
1424
|
+
*/
|
|
1425
|
+
const summarizeMemory = async (sessionId) => {
|
|
1426
|
+
const response = await fetch(summarizeUrl(sessionId), {
|
|
1427
|
+
method: "POST",
|
|
1428
|
+
credentials: "same-origin",
|
|
1429
|
+
headers: { accept: "application/json" }
|
|
1430
|
+
});
|
|
1431
|
+
const body = await response.json().catch(() => void 0);
|
|
1432
|
+
if (!response.ok) {
|
|
1433
|
+
const message = isRecord(body) && typeof body.error === "string" ? body.error : `HTTP ${String(response.status)}`;
|
|
1434
|
+
throw new Error(message);
|
|
1435
|
+
}
|
|
1436
|
+
const result = parseSummarizeResult(body, sessionId);
|
|
1437
|
+
if (result === void 0) throw new Error("the Host returned an unreadable consolidation answer");
|
|
1438
|
+
return result;
|
|
1439
|
+
};
|
|
1440
|
+
/**
|
|
1441
|
+
* Read the Host's consolidation answer, defensively.
|
|
1442
|
+
*
|
|
1443
|
+
* A body without a boolean `queued` is unreadable rather than "not queued": the
|
|
1444
|
+
* whole point of the answer is to distinguish "accepted" from "nothing to do",
|
|
1445
|
+
* and guessing would put a wrong sentence in front of the user.
|
|
1446
|
+
*
|
|
1447
|
+
* @param value - the parsed response body.
|
|
1448
|
+
* @param sessionId - the session that was asked for.
|
|
1449
|
+
* @returns the result, or undefined when the body is not one.
|
|
1450
|
+
*/
|
|
1451
|
+
function parseSummarizeResult(value, sessionId) {
|
|
1452
|
+
if (!isRecord(value) || typeof value.queued !== "boolean") return void 0;
|
|
1453
|
+
return {
|
|
1454
|
+
sessionId: str(value.sessionId) || sessionId,
|
|
1455
|
+
queued: value.queued,
|
|
1456
|
+
reason: str(value.reason),
|
|
1457
|
+
fromSeq: num(value.fromSeq),
|
|
1458
|
+
toSeq: num(value.toSeq)
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
/**
|
|
1371
1462
|
* Fetch the shelf listing for the settings card.
|
|
1372
1463
|
*
|
|
1373
1464
|
* Rides the same self-authenticating route family as the Memory tab — the
|
|
@@ -1531,7 +1622,7 @@ window.__ModuleLoader__.load({
|
|
|
1531
1622
|
}
|
|
1532
1623
|
//#endregion
|
|
1533
1624
|
//#region \0dsh-hypatia-css:/home/runner/work/dsh-hypatia-auto-memory/dsh-hypatia-auto-memory/src/client/MemoryView.module.css.mjs
|
|
1534
|
-
const css = ".LBuOzG_panel{box-sizing:border-box;width:100%;height:100%;min-height:0;padding:16px;padding-bottom:calc(var(--dsh-composer-height,152px) + 16px);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-1);flex-direction:column;align-items:center;display:flex;overflow-y:auto}.LBuOzG_column{flex-direction:column;gap:20px;width:100%;min-width:0;max-width:880px;display:flex}.LBuOzG_head{justify-content:space-between;align-items:baseline;gap:12px;display:flex}.LBuOzG_title{color:var(--dsw-alias-label-primary);margin:0;font-size:15px;font-weight:600;line-height:1.4}.LBuOzG_headMeta{align-items:center;gap:10px;display:flex}.LBuOzG_updated{color:var(--dsw-alias-label-tertiary);font-size:12px}.LBuOzG_refresh{appearance:none;border:.5px solid var(--dsw-alias-border-l4);color:var(--dsw-alias-label-secondary);cursor:pointer;font:inherit;background:0 0;border-radius:8px;padding:2px 8px;font-size:12px}.LBuOzG_refresh:hover{border-color:var(--dsw-alias-label-dimmed);color:var(--dsw-alias-label-primary)}.LBuOzG_refresh:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}.LBuOzG_status,.LBuOzG_hint,.LBuOzG_error{margin:0;font-size:12px;line-height:1.6}.LBuOzG_status,.LBuOzG_hint{color:var(--dsw-alias-label-tertiary)}.LBuOzG_error{color:var(--dsw-alias-label-error)}.LBuOzG_group{border-top:.5px solid var(--dsw-alias-border-l2);flex-direction:column;gap:10px;padding-top:16px;display:flex}.LBuOzG_groupTitle{color:var(--dsw-alias-label-primary);align-items:baseline;gap:8px;margin:0;font-size:13px;font-weight:600;display:flex}.LBuOzG_count{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;font-size:11px;font-weight:400}.LBuOzG_rows{grid-template-columns:max-content minmax(0,1fr);align-items:baseline;gap:7px 20px;margin:0;display:grid}.LBuOzG_row{display:contents}.LBuOzG_label{color:var(--dsw-alias-label-tertiary);white-space:nowrap;font-size:12px}.LBuOzG_value{min-width:0;color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums;align-items:baseline;gap:8px;margin:0;font-size:12px;display:flex}.LBuOzG_calm,.LBuOzG_busy{white-space:nowrap;border-radius:6px;padding:1px 6px;font-size:11px}.LBuOzG_calm{color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-bg-layer-2)}.LBuOzG_busy{color:var(--dsw-alias-label-primary);border:.5px solid var(--dsw-alias-border-l4);background:var(--dsw-alias-bg-layer-3)}.LBuOzG_failures{gap:6px;margin:0;padding:0;list-style:none;display:grid}.LBuOzG_failure{background:var(--dsw-alias-bg-layer-2);border-radius:8px;gap:2px;padding:8px 10px;display:grid}.LBuOzG_failureKind{color:var(--dsw-alias-label-primary);font-size:12px}.LBuOzG_failureError{color:var(--dsw-alias-label-tertiary);overflow-wrap:anywhere;font-size:11px;line-height:1.5}.LBuOzG_entries{flex-direction:column;gap:8px;margin:0;padding:0;list-style:none;display:flex}.LBuOzG_entry{border:.5px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);border-radius:10px;overflow:hidden}.LBuOzG_entryHead{appearance:none;width:100%;color:inherit;cursor:pointer;font:inherit;text-align:left;background:0 0;border:0;justify-content:space-between;align-items:baseline;gap:12px;padding:8px 12px;display:flex}.LBuOzG_entryHead:hover{background:var(--dsw-alias-bg-layer-3)}.LBuOzG_entryHead:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}.LBuOzG_entryName{min-width:0;color:var(--dsw-alias-label-secondary);font-family:var(--dsw-font-family-mono,ui-monospace, SFMono-Regular, Menlo, monospace);text-overflow:ellipsis;white-space:nowrap;font-size:11px;overflow:hidden}.LBuOzG_entryMeta{flex:none;align-items:baseline;gap:8px;display:flex}.LBuOzG_badge{background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-tertiary);white-space:nowrap;border-radius:6px;padding:1px 6px;font-size:11px}.LBuOzG_entryTime{color:var(--dsw-alias-label-quaternary);font-variant-numeric:tabular-nums;white-space:nowrap;font-size:11px}.LBuOzG_entryToggle{color:var(--dsw-alias-label-tertiary);white-space:nowrap;font-size:11px}.LBuOzG_entryBody{border-top:.5px solid var(--dsw-alias-border-l2);overflow-wrap:anywhere;padding:10px 12px;font-size:13px;line-height:1.65}.LBuOzG_entryPreview{color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;margin:0;padding:0 12px 10px;font-size:12px;line-height:1.5;overflow:hidden}.LBuOzG_hint{border-top:.5px solid var(--dsw-alias-border-l2);padding-top:12px}";
|
|
1625
|
+
const css = ".LBuOzG_panel{box-sizing:border-box;width:100%;height:100%;min-height:0;padding:16px;padding-bottom:calc(var(--dsh-composer-height,152px) + 16px);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-1);flex-direction:column;align-items:center;display:flex;overflow-y:auto}.LBuOzG_column{flex-direction:column;gap:20px;width:100%;min-width:0;max-width:880px;display:flex}.LBuOzG_head{justify-content:space-between;align-items:baseline;gap:12px;display:flex}.LBuOzG_title{color:var(--dsw-alias-label-primary);margin:0;font-size:15px;font-weight:600;line-height:1.4}.LBuOzG_headMeta{align-items:center;gap:10px;display:flex}.LBuOzG_updated{color:var(--dsw-alias-label-tertiary);font-size:12px}.LBuOzG_refresh{appearance:none;border:.5px solid var(--dsw-alias-border-l4);color:var(--dsw-alias-label-secondary);cursor:pointer;font:inherit;background:0 0;border-radius:8px;padding:2px 8px;font-size:12px}.LBuOzG_refresh:hover{border-color:var(--dsw-alias-label-dimmed);color:var(--dsw-alias-label-primary)}.LBuOzG_refresh:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}.LBuOzG_status,.LBuOzG_hint,.LBuOzG_error,.LBuOzG_notice{margin:0;font-size:12px;line-height:1.6}.LBuOzG_status,.LBuOzG_hint{color:var(--dsw-alias-label-tertiary)}.LBuOzG_error{color:var(--dsw-alias-label-error)}.LBuOzG_notice{color:var(--dsw-alias-label-secondary)}.LBuOzG_group{border-top:.5px solid var(--dsw-alias-border-l2);flex-direction:column;gap:10px;padding-top:16px;display:flex}.LBuOzG_groupTitle{color:var(--dsw-alias-label-primary);align-items:baseline;gap:8px;margin:0;font-size:13px;font-weight:600;display:flex}.LBuOzG_count{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;font-size:11px;font-weight:400}.LBuOzG_rows{grid-template-columns:max-content minmax(0,1fr);align-items:baseline;gap:7px 20px;margin:0;display:grid}.LBuOzG_row{display:contents}.LBuOzG_label{color:var(--dsw-alias-label-tertiary);white-space:nowrap;font-size:12px}.LBuOzG_value{min-width:0;color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums;align-items:baseline;gap:8px;margin:0;font-size:12px;display:flex}.LBuOzG_calm,.LBuOzG_busy{white-space:nowrap;border-radius:6px;padding:1px 6px;font-size:11px}.LBuOzG_calm{color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-bg-layer-2)}.LBuOzG_busy{color:var(--dsw-alias-label-primary);border:.5px solid var(--dsw-alias-border-l4);background:var(--dsw-alias-bg-layer-3)}.LBuOzG_summarize{appearance:none;border:.5px solid var(--dsw-alias-border-l4);color:var(--dsw-alias-label-secondary);cursor:pointer;font:inherit;white-space:nowrap;background:0 0;border-radius:6px;padding:1px 8px;font-size:11px}.LBuOzG_summarize:hover:enabled{border-color:var(--dsw-alias-label-dimmed);color:var(--dsw-alias-label-primary)}.LBuOzG_summarize:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}.LBuOzG_summarize:disabled{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-dimmed);cursor:default}.LBuOzG_failures{gap:6px;margin:0;padding:0;list-style:none;display:grid}.LBuOzG_failure{background:var(--dsw-alias-bg-layer-2);border-radius:8px;gap:2px;padding:8px 10px;display:grid}.LBuOzG_failureKind{color:var(--dsw-alias-label-primary);font-size:12px}.LBuOzG_failureError{color:var(--dsw-alias-label-tertiary);overflow-wrap:anywhere;font-size:11px;line-height:1.5}.LBuOzG_entries{flex-direction:column;gap:8px;margin:0;padding:0;list-style:none;display:flex}.LBuOzG_entry{border:.5px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);border-radius:10px;overflow:hidden}.LBuOzG_entryHead{appearance:none;width:100%;color:inherit;cursor:pointer;font:inherit;text-align:left;background:0 0;border:0;justify-content:space-between;align-items:baseline;gap:12px;padding:8px 12px;display:flex}.LBuOzG_entryHead:hover{background:var(--dsw-alias-bg-layer-3)}.LBuOzG_entryHead:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}.LBuOzG_entryName{min-width:0;color:var(--dsw-alias-label-secondary);font-family:var(--dsw-font-family-mono,ui-monospace, SFMono-Regular, Menlo, monospace);text-overflow:ellipsis;white-space:nowrap;font-size:11px;overflow:hidden}.LBuOzG_entryMeta{flex:none;align-items:baseline;gap:8px;display:flex}.LBuOzG_badge{background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-tertiary);white-space:nowrap;border-radius:6px;padding:1px 6px;font-size:11px}.LBuOzG_entryTime{color:var(--dsw-alias-label-quaternary);font-variant-numeric:tabular-nums;white-space:nowrap;font-size:11px}.LBuOzG_entryToggle{color:var(--dsw-alias-label-tertiary);white-space:nowrap;font-size:11px}.LBuOzG_entryBody{border-top:.5px solid var(--dsw-alias-border-l2);overflow-wrap:anywhere;padding:10px 12px;font-size:13px;line-height:1.65}.LBuOzG_entryPreview{color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;margin:0;padding:0 12px 10px;font-size:12px;line-height:1.5;overflow:hidden}.LBuOzG_hint{border-top:.5px solid var(--dsw-alias-border-l2);padding-top:12px}";
|
|
1535
1626
|
const tagId = "dsh-hypatia-auto-memory/MemoryView.module.css";
|
|
1536
1627
|
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
1537
1628
|
const tag = document.createElement("style");
|
|
@@ -1566,11 +1657,13 @@ window.__ModuleLoader__.load({
|
|
|
1566
1657
|
"headMeta": "LBuOzG_headMeta",
|
|
1567
1658
|
"hint": "LBuOzG_hint",
|
|
1568
1659
|
"label": "LBuOzG_label",
|
|
1660
|
+
"notice": "LBuOzG_notice",
|
|
1569
1661
|
"panel": "LBuOzG_panel",
|
|
1570
1662
|
"refresh": "LBuOzG_refresh",
|
|
1571
1663
|
"row": "LBuOzG_row",
|
|
1572
1664
|
"rows": "LBuOzG_rows",
|
|
1573
1665
|
"status": "LBuOzG_status",
|
|
1666
|
+
"summarize": "LBuOzG_summarize",
|
|
1574
1667
|
"title": "LBuOzG_title",
|
|
1575
1668
|
"updated": "LBuOzG_updated",
|
|
1576
1669
|
"value": "LBuOzG_value"
|
|
@@ -1598,6 +1691,48 @@ window.__ModuleLoader__.load({
|
|
|
1598
1691
|
*/
|
|
1599
1692
|
/** How often the tab re-reads the status half while it is on screen. */
|
|
1600
1693
|
const POLL_MS = 5e3;
|
|
1694
|
+
/**
|
|
1695
|
+
* Render the Host's answer to one request.
|
|
1696
|
+
*
|
|
1697
|
+
* "Nothing to do" and "already queued" are not failures and do not read as one:
|
|
1698
|
+
* the request was answered truthfully. Only a refusal — the feature is off, the
|
|
1699
|
+
* route is missing, the session is gone — is shown as an error.
|
|
1700
|
+
*/
|
|
1701
|
+
function noticeFor(result, t) {
|
|
1702
|
+
if (result.queued) return {
|
|
1703
|
+
kind: "ok",
|
|
1704
|
+
text: t("memorySummarizeQueued", {
|
|
1705
|
+
from: String(result.fromSeq),
|
|
1706
|
+
to: String(result.toSeq)
|
|
1707
|
+
})
|
|
1708
|
+
};
|
|
1709
|
+
switch (result.reason) {
|
|
1710
|
+
case SUMMARIZE_REASON.empty: return {
|
|
1711
|
+
kind: "ok",
|
|
1712
|
+
text: t("memorySummarizeEmpty")
|
|
1713
|
+
};
|
|
1714
|
+
case SUMMARIZE_REASON.busy: return {
|
|
1715
|
+
kind: "ok",
|
|
1716
|
+
text: t("memorySummarizeBusy")
|
|
1717
|
+
};
|
|
1718
|
+
case SUMMARIZE_REASON.disabled: return {
|
|
1719
|
+
kind: "error",
|
|
1720
|
+
text: t("memorySummarizeDisabled")
|
|
1721
|
+
};
|
|
1722
|
+
case SUMMARIZE_REASON.unavailable: return {
|
|
1723
|
+
kind: "error",
|
|
1724
|
+
text: t("memorySummarizeUnavailable")
|
|
1725
|
+
};
|
|
1726
|
+
case SUMMARIZE_REASON.unknownSession: return {
|
|
1727
|
+
kind: "error",
|
|
1728
|
+
text: t("memorySummarizeUnknown")
|
|
1729
|
+
};
|
|
1730
|
+
default: return {
|
|
1731
|
+
kind: "error",
|
|
1732
|
+
text: t("memorySummarizeUnknownReason", { reason: result.reason })
|
|
1733
|
+
};
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1601
1736
|
/** Local wall-clock stamp for the freshness line. */
|
|
1602
1737
|
function formatTime(epochMs) {
|
|
1603
1738
|
if (!Number.isFinite(epochMs) || epochMs <= 0) return "—";
|
|
@@ -1664,10 +1799,14 @@ window.__ModuleLoader__.load({
|
|
|
1664
1799
|
})]
|
|
1665
1800
|
});
|
|
1666
1801
|
}
|
|
1667
|
-
function MemoryView({ sessionId, fetch, t }) {
|
|
1802
|
+
function MemoryView({ sessionId, fetch, summarize, t }) {
|
|
1668
1803
|
const [payload, setPayload] = (0, react.useState)(void 0);
|
|
1669
1804
|
const [loading, setLoading] = (0, react.useState)(true);
|
|
1670
1805
|
const [error, setError] = (0, react.useState)("");
|
|
1806
|
+
/** True while the Host is being asked to queue a consolidation. */
|
|
1807
|
+
const [summarizing, setSummarizing] = (0, react.useState)(false);
|
|
1808
|
+
/** The last explicit request's answer, cleared when the session switches. */
|
|
1809
|
+
const [notice, setNotice] = (0, react.useState)(void 0);
|
|
1671
1810
|
/**
|
|
1672
1811
|
* The entries the reader opened, by name. Absent means closed, which is the
|
|
1673
1812
|
* default: a toggle survives the poll that replaces the payload, and an entry
|
|
@@ -1716,6 +1855,7 @@ window.__ModuleLoader__.load({
|
|
|
1716
1855
|
setPayload(void 0);
|
|
1717
1856
|
setOverrides({});
|
|
1718
1857
|
setContentAt(void 0);
|
|
1858
|
+
setNotice(void 0);
|
|
1719
1859
|
setLoading(true);
|
|
1720
1860
|
load(true);
|
|
1721
1861
|
}, [load, sessionId]);
|
|
@@ -1741,6 +1881,39 @@ window.__ModuleLoader__.load({
|
|
|
1741
1881
|
[name]: !open
|
|
1742
1882
|
}));
|
|
1743
1883
|
}, []);
|
|
1884
|
+
/**
|
|
1885
|
+
* Ask the Host to consolidate this session now.
|
|
1886
|
+
*
|
|
1887
|
+
* Only queues: the summary is written in the background, so the answer says
|
|
1888
|
+
* what was accepted rather than what was written. A queued request refreshes
|
|
1889
|
+
* the status so the "behind" figure catches up with the next poll; the
|
|
1890
|
+
* watermark watch above then re-reads the shelf when consolidation lands.
|
|
1891
|
+
*/
|
|
1892
|
+
const onSummarize = (0, react.useCallback)(() => {
|
|
1893
|
+
setSummarizing(true);
|
|
1894
|
+
setNotice(void 0);
|
|
1895
|
+
(async () => {
|
|
1896
|
+
try {
|
|
1897
|
+
const result = await summarize(sessionId);
|
|
1898
|
+
if (activeSession.current !== sessionId) return;
|
|
1899
|
+
setNotice(noticeFor(result, t));
|
|
1900
|
+
if (result.queued) load(false);
|
|
1901
|
+
} catch (failure) {
|
|
1902
|
+
if (activeSession.current !== sessionId) return;
|
|
1903
|
+
setNotice({
|
|
1904
|
+
kind: "error",
|
|
1905
|
+
text: t("memorySummarizeFailed", { message: failure instanceof Error ? failure.message : String(failure) })
|
|
1906
|
+
});
|
|
1907
|
+
} finally {
|
|
1908
|
+
if (activeSession.current === sessionId) setSummarizing(false);
|
|
1909
|
+
}
|
|
1910
|
+
})();
|
|
1911
|
+
}, [
|
|
1912
|
+
load,
|
|
1913
|
+
sessionId,
|
|
1914
|
+
summarize,
|
|
1915
|
+
t
|
|
1916
|
+
]);
|
|
1744
1917
|
const session = payload?.session;
|
|
1745
1918
|
const content = payload?.content;
|
|
1746
1919
|
const summaries = content?.summaries ?? [];
|
|
@@ -1790,6 +1963,11 @@ window.__ModuleLoader__.load({
|
|
|
1790
1963
|
role: "status",
|
|
1791
1964
|
children: t("memoryLoadFailed", { message: error })
|
|
1792
1965
|
}) : null,
|
|
1966
|
+
notice !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1967
|
+
className: notice.kind === "ok" ? MemoryView_module_css_default.notice : MemoryView_module_css_default.error,
|
|
1968
|
+
role: "status",
|
|
1969
|
+
children: notice.text
|
|
1970
|
+
}) : null,
|
|
1793
1971
|
loading && payload === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1794
1972
|
className: MemoryView_module_css_default.status,
|
|
1795
1973
|
children: t("memoryLoading")
|
|
@@ -1822,10 +2000,21 @@ window.__ModuleLoader__.load({
|
|
|
1822
2000
|
children: t("memoryConsolidated")
|
|
1823
2001
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dd", {
|
|
1824
2002
|
className: MemoryView_module_css_default.value,
|
|
1825
|
-
children: [
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
2003
|
+
children: [
|
|
2004
|
+
t("memorySeqValue", { seq: String(session.consolidatedSeq) }),
|
|
2005
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2006
|
+
className: session.caughtUp ? MemoryView_module_css_default.calm : MemoryView_module_css_default.busy,
|
|
2007
|
+
children: session.caughtUp ? t("memoryCaughtUp") : t("memoryBehind", { behind: String(consolidationGap(session)) })
|
|
2008
|
+
}),
|
|
2009
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2010
|
+
type: "button",
|
|
2011
|
+
className: MemoryView_module_css_default.summarize,
|
|
2012
|
+
disabled: summarizing || !canSummarize(session),
|
|
2013
|
+
title: t("memorySummarizeHint"),
|
|
2014
|
+
onClick: onSummarize,
|
|
2015
|
+
children: summarizing ? t("memorySummarizing") : t("memorySummarize")
|
|
2016
|
+
})
|
|
2017
|
+
]
|
|
1829
2018
|
})]
|
|
1830
2019
|
}),
|
|
1831
2020
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -2146,7 +2335,10 @@ window.__ModuleLoader__.load({
|
|
|
2146
2335
|
order: 20,
|
|
2147
2336
|
locale: NS,
|
|
2148
2337
|
label: () => t("viewMemory"),
|
|
2149
|
-
inject: () => ({
|
|
2338
|
+
inject: () => ({
|
|
2339
|
+
fetch: fetchMemory,
|
|
2340
|
+
summarize: summarizeMemory
|
|
2341
|
+
})
|
|
2150
2342
|
}, MemoryView));
|
|
2151
2343
|
ctx.effect(() => () => {
|
|
2152
2344
|
unregister();
|