dsh-archived-chats 1.0.4 → 1.0.6
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 +5 -1
- package/README.zh-CN.md +5 -1
- package/docs/ARCHITECTURE.en.md +1 -1
- package/docs/ARCHITECTURE.md +1 -1
- package/lib/index.js +13 -3
- package/lib/lineage.js +30 -18
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -116,7 +116,11 @@ The eight fixed screenshots below come from an isolated Simplified Chinese light
|
|
|
116
116
|
| Architecture | [Maintainer architecture](docs/ARCHITECTURE.en.md) | [维护者架构](docs/ARCHITECTURE.md) |
|
|
117
117
|
| Release history | [GitHub Releases](https://github.com/Ultronen/dsh-archived-chats/releases) | [GitHub Releases](https://github.com/Ultronen/dsh-archived-chats/releases) |
|
|
118
118
|
|
|
119
|
-
See also [Security](SECURITY.md), [Contributing](CONTRIBUTING.md), and [Discussions](https://github.com/Ultronen/dsh-archived-chats/discussions).
|
|
119
|
+
See also [Support](SUPPORT.md), [Security](SECURITY.md), [Contributing](CONTRIBUTING.md), [Code of Conduct](CODE_OF_CONDUCT.md), and [Discussions](https://github.com/Ultronen/dsh-archived-chats/discussions).
|
|
120
|
+
|
|
121
|
+
## Project status
|
|
122
|
+
|
|
123
|
+
Session Archive is actively maintained. The latest stable npm release receives fixes and security updates; older releases should be upgraded before reporting a problem. Reproducible bug reports and focused pull requests are welcome. Maintenance is performed as availability permits, so no fixed response or release schedule is promised.
|
|
120
124
|
|
|
121
125
|
## Development
|
|
122
126
|
|
package/README.zh-CN.md
CHANGED
|
@@ -116,7 +116,11 @@ dsh plugin --profile web update dsh-archived-chats
|
|
|
116
116
|
| 架构说明 | [Maintainer architecture](docs/ARCHITECTURE.en.md) | [维护者架构](docs/ARCHITECTURE.md) |
|
|
117
117
|
| 版本历史 | [GitHub Releases](https://github.com/Ultronen/dsh-archived-chats/releases) | [GitHub Releases](https://github.com/Ultronen/dsh-archived-chats/releases) |
|
|
118
118
|
|
|
119
|
-
另见 [安全说明](SECURITY.md)、[贡献指南](CONTRIBUTING.md)和[问题交流](https://github.com/Ultronen/dsh-archived-chats/discussions)。
|
|
119
|
+
另见 [支持说明](SUPPORT.md)、[安全说明](SECURITY.md)、[贡献指南](CONTRIBUTING.md)、[行为准则](CODE_OF_CONDUCT.md)和[问题交流](https://github.com/Ultronen/dsh-archived-chats/discussions)。
|
|
120
|
+
|
|
121
|
+
## 项目状态
|
|
122
|
+
|
|
123
|
+
Session Archive 目前处于积极维护状态。最新 npm 稳定版会接收缺陷修复与安全更新;报告问题前请先从旧版本升级。欢迎可复现的缺陷报告和目标集中的 Pull Request。项目按维护者的可用时间推进,不承诺固定响应或发布时间。
|
|
120
124
|
|
|
121
125
|
## 开发
|
|
122
126
|
|
package/docs/ARCHITECTURE.en.md
CHANGED
|
@@ -68,7 +68,7 @@ Metadata and recycle catalogs are versioned. Writes serialize and atomically rep
|
|
|
68
68
|
|
|
69
69
|
The stats route measures session directories with concurrency four, skips symbolic links, and caches results for 30 seconds. A measurement failure marks only that row unavailable; list and mutation actions continue. Delete invalidates the affected cache row.
|
|
70
70
|
|
|
71
|
-
Insights joins session measurement with a stream-verified snapshot inventory and counts repeated snapshot attachments only from validated SHA-256 descriptors. The browser keeps totals in summary cards and presents session/snapshot inventories only in bounded searchable dialogs. retention.json uses an exact version-one schema; saving never runs cleanup. Preview issues a five-minute single-use token/nonce and apply revalidates inside the lifecycle queue; recycle candidates still delegate to recycle purge. Lineage uses only durable parentSession edges,
|
|
71
|
+
Insights joins session measurement with a stream-verified snapshot inventory and counts repeated snapshot attachments only from validated SHA-256 descriptors. The browser keeps totals in summary cards and presents session/snapshot inventories only in bounded searchable dialogs. retention.json uses an exact version-one schema; saving never runs cleanup. Preview issues a five-minute single-use token/nonce and apply revalidates inside the lifecycle queue; recycle candidates still delegate to recycle purge. Lineage uses only durable parentSession edges, never rewrites headers, and resolves titles for at most 100 untitled active source nodes already included in the focused tree. Its 5,000-node cap bounds the PROJECTED graph, not the Host's store: `focusIds` narrows the output to archived and recycled chats plus their explaining context, so a store of 100,000 sessions with 25 archived projects 25 nodes. Header fields are coerced per node rather than validated whole-graph — an unanticipated `origin` value, an absent `createdAt`, a changed numeric type each degrade that node to a value the public LineageNode already allows, and only an unusable identity drops a row. A projection that rejected the whole graph for one unrecognized header let a Host-side change disable the panel with no plugin change at all; malformed workspace and recycle entries are skipped for the same reason.
|
|
72
72
|
|
|
73
73
|
## Preview and full-text search
|
|
74
74
|
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -68,7 +68,7 @@ $DSH_HOME/plugin-data/archived-chats/snapshots/
|
|
|
68
68
|
|
|
69
69
|
stats 路由以并发 4 测量会话目录,跳过符号链接,结果缓存 30 秒。测量失败只标记当前行不可用,不阻塞列表和其他操作;删除会使对应缓存失效。
|
|
70
70
|
|
|
71
|
-
insights 将会话目录测量与流式校验的快照清单分账,重复附件只按快照内已验证 SHA-256 统计;浏览器只在摘要卡片中显示总量,会话目录和快照明细通过有界、可搜索弹窗按需呈现。retention.json 使用精确 version 1 schema;保存策略不执行清理。preview 生成五分钟、单次使用的 token/nonce,apply 在生命周期队列内重检候选,回收站候选仍委托 recycle purge。lineage 只用持久化 parentSession
|
|
71
|
+
insights 将会话目录测量与流式校验的快照清单分账,重复附件只按快照内已验证 SHA-256 统计;浏览器只在摘要卡片中显示总量,会话目录和快照明细通过有界、可搜索弹窗按需呈现。retention.json 使用精确 version 1 schema;保存策略不执行清理。preview 生成五分钟、单次使用的 token/nonce,apply 在生命周期队列内重检候选,回收站候选仍委托 recycle purge。lineage 只用持久化 parentSession 建树,不修改会话头;对已经进入聚焦关系树但没有安全标题的活动来源节点,最多按需读取 100 个标题事件。其 5,000 节点上限约束的是**实际投影出的**关系图,而不是 Host 的会话库:`focusIds` 已把输出收窄为归档与回收聊天加上解释它们所需的上下文,因此 100,000 个会话、25 条归档只投影 25 个节点。会话头字段按节点逐项收敛而不是整图校验——未预期的 `origin` 取值、缺失的 `createdAt`、变化的数值类型,各自只把该节点降级为公开 LineageNode 已允许的取值,只有身份不可用才丢弃该行。为一个不认识的会话头拒绝整张图,会让 Host 侧的变化在插件毫无改动的情况下直接停用该面板;畸形的工作区与回收记录出于同样理由被跳过。
|
|
72
72
|
|
|
73
73
|
## 预览和全文搜索
|
|
74
74
|
|
package/lib/index.js
CHANGED
|
@@ -518,7 +518,10 @@ async function resolveLineageContextTitles(graph, persistence) {
|
|
|
518
518
|
* Delete and unarchive invalidate their ids.
|
|
519
519
|
*/
|
|
520
520
|
async function listArchived(ctx, registry, persistence, titleCache, metadataStore, pendingStore, trashStore) {
|
|
521
|
-
|
|
521
|
+
// The archive set is Host state. A repeated id there is meaningless to every
|
|
522
|
+
// consumer here, and would otherwise list one chat twice or fail a reader
|
|
523
|
+
// that treats its own row set as unique.
|
|
524
|
+
const archivedIds = [...new Set(registry.archivedSessionIds.map(String))];
|
|
522
525
|
if (archivedIds.length === 0) {
|
|
523
526
|
const metadata = await metadataStore.getMany([]);
|
|
524
527
|
const trash = await trashStore.load();
|
|
@@ -818,7 +821,7 @@ function registerRoutes(ctx, webServer, registry, persistence, titleCache, metad
|
|
|
818
821
|
try { headers = await persistence.list(); } catch { /* Rows remain measurable as unavailable. */ }
|
|
819
822
|
const headerById = new Map((Array.isArray(headers) ? headers : []).map((header) => [String(header.id), header]));
|
|
820
823
|
const workspaces = registry.list();
|
|
821
|
-
return registry.archivedSessionIds.map(String).filter((id) => !pending.has(id)).map((id) => {
|
|
824
|
+
return [...new Set(registry.archivedSessionIds.map(String))].filter((id) => !pending.has(id)).map((id) => {
|
|
822
825
|
const record = trash.records.get(id);
|
|
823
826
|
const header = headerById.get(id) ?? ctx.get('sessions')?.get(id)?.header;
|
|
824
827
|
const workspace = record?.workspace ?? workspaces.find((item) => workspaceSessionIds(item).includes(id));
|
|
@@ -1373,7 +1376,14 @@ function registerRoutes(ctx, webServer, registry, persistence, titleCache, metad
|
|
|
1373
1376
|
res.destroy?.();
|
|
1374
1377
|
return;
|
|
1375
1378
|
}
|
|
1376
|
-
|
|
1379
|
+
// Selection parsing attaches its own 400/413, so anything arriving
|
|
1380
|
+
// without a status is an internal failure. Reporting those as
|
|
1381
|
+
// `invalid-export-request` blamed the caller for a bug here and
|
|
1382
|
+
// left nothing in the log to diagnose it.
|
|
1383
|
+
const status = error.status ?? 500;
|
|
1384
|
+
if (status >= 500) {
|
|
1385
|
+
ctx.logger.warn(`archived-chats: export failed: ${String(error?.code ?? error?.name ?? 'Error')}`);
|
|
1386
|
+
}
|
|
1377
1387
|
sendText(res, status, status === 413
|
|
1378
1388
|
? 'request-too-large'
|
|
1379
1389
|
: (status === 400 ? 'invalid-export-request' : 'export-failed'));
|
package/lib/lineage.js
CHANGED
|
@@ -65,13 +65,25 @@ export function projectLineage({
|
|
|
65
65
|
|| !(trashRecords instanceof Map) || !(titles instanceof Map) || !Array.isArray(workspaces)
|
|
66
66
|
|| (focusIds !== undefined && (!Array.isArray(focusIds)
|
|
67
67
|
|| focusIds.some((id) => typeof id !== 'string' || id === '')))) throw invalid();
|
|
68
|
-
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The node limit bounds the graph this projection RETURNS, not how many
|
|
71
|
+
* sessions the Host happens to store. `focusIds` already narrows the output to
|
|
72
|
+
* the archived and recycled chats plus the context that explains them, so a
|
|
73
|
+
* store of 100,000 sessions with 25 archived projects 25 nodes. Counting the
|
|
74
|
+
* input instead refused that outright, and the measured cost of the wide input
|
|
75
|
+
* it was guarding against is milliseconds.
|
|
76
|
+
*/
|
|
77
|
+
const withinLimit = (count) => {
|
|
78
|
+
if (count > maxNodes) throw new LineageError('lineage-limit-exceeded', 'lineage exceeds the node limit', 413);
|
|
79
|
+
return count;
|
|
80
|
+
};
|
|
69
81
|
|
|
70
82
|
const archived = new Set((Array.isArray(archivedIds) ? archivedIds : []).map(String));
|
|
71
83
|
const trashed = new Set([...trashRecords.keys()].map(String));
|
|
72
84
|
const workspaceBySession = new Map();
|
|
73
85
|
for (const workspace of workspaces) {
|
|
74
|
-
if (workspace === null || typeof workspace !== 'object')
|
|
86
|
+
if (workspace === null || typeof workspace !== 'object') continue;
|
|
75
87
|
const safe = {
|
|
76
88
|
id: typeof workspace.id === 'string' ? workspace.id : null,
|
|
77
89
|
title: typeof workspace.title === 'string' ? workspace.title : null,
|
|
@@ -82,25 +94,24 @@ export function projectLineage({
|
|
|
82
94
|
const nodes = new Map();
|
|
83
95
|
const order = [];
|
|
84
96
|
for (const header of headers) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
97
|
+
// Headers are the Host's data, so every field here is a forward-compatibility
|
|
98
|
+
// surface: a new `origin` value, an absent timestamp, a changed numeric type.
|
|
99
|
+
// Each one degrades that single node's detail to a value the public
|
|
100
|
+
// LineageNode already allows. Only an unusable identity drops the row, and a
|
|
101
|
+
// duplicate id keeps the first. One unrecognized session must never cost the
|
|
102
|
+
// whole graph — this projection was the only surface that failed that way.
|
|
103
|
+
if (header === null || typeof header !== 'object' || Array.isArray(header)) continue;
|
|
104
|
+
if (typeof header.id !== 'string' || header.id === '' || nodes.has(header.id)) continue;
|
|
94
105
|
const id = header.id;
|
|
95
106
|
const cachedTitle = titles.get(id);
|
|
96
107
|
const node = {
|
|
97
108
|
id,
|
|
98
|
-
parentSession: header.parentSession
|
|
99
|
-
seedLength: header.seedLength
|
|
100
|
-
origin: header.origin
|
|
101
|
-
delegationDepth: header.delegationDepth
|
|
109
|
+
parentSession: typeof header.parentSession === 'string' && header.parentSession !== '' ? header.parentSession : null,
|
|
110
|
+
seedLength: Number.isSafeInteger(header.seedLength) && header.seedLength >= 0 ? header.seedLength : null,
|
|
111
|
+
origin: header.origin === 'subagent' ? 'subagent' : null,
|
|
112
|
+
delegationDepth: Number.isSafeInteger(header.delegationDepth) && header.delegationDepth >= 0 ? header.delegationDepth : 0,
|
|
102
113
|
title: typeof cachedTitle === 'string' ? cachedTitle : (typeof header.title === 'string' ? header.title : null),
|
|
103
|
-
createdAt: header.createdAt,
|
|
114
|
+
createdAt: Number.isSafeInteger(header.createdAt) && header.createdAt >= 0 ? header.createdAt : null,
|
|
104
115
|
workspace: { ...(workspaceBySession.get(id) ?? { id: null, title: null }) },
|
|
105
116
|
status: trashed.has(id) ? 'trash' : archived.has(id) ? 'archived' : 'active',
|
|
106
117
|
children: [],
|
|
@@ -112,8 +123,7 @@ export function projectLineage({
|
|
|
112
123
|
for (const [key, record] of trashRecords) {
|
|
113
124
|
const id = String(key);
|
|
114
125
|
if (nodes.has(id)) continue;
|
|
115
|
-
if (record === null || typeof record !== 'object' || String(record.sessionId) !== id)
|
|
116
|
-
if (nodes.size >= maxNodes) throw new LineageError('lineage-limit-exceeded', 'lineage exceeds the node limit', 413);
|
|
126
|
+
if (record === null || typeof record !== 'object' || String(record.sessionId) !== id) continue;
|
|
117
127
|
const workspace = record.workspace;
|
|
118
128
|
nodes.set(id, {
|
|
119
129
|
id,
|
|
@@ -232,6 +242,7 @@ export function projectLineage({
|
|
|
232
242
|
current = parent;
|
|
233
243
|
}
|
|
234
244
|
}
|
|
245
|
+
withinLimit(included.size);
|
|
235
246
|
return {
|
|
236
247
|
roots: cloneForest(roots, included),
|
|
237
248
|
diagnostics: diagnostics.filter((item) => included.has(item.sessionId)).map((item) => ({ ...item })),
|
|
@@ -239,6 +250,7 @@ export function projectLineage({
|
|
|
239
250
|
};
|
|
240
251
|
}
|
|
241
252
|
|
|
253
|
+
withinLimit(nodes.size + missingNodes.size);
|
|
242
254
|
return {
|
|
243
255
|
roots: cloneForest(roots),
|
|
244
256
|
diagnostics: diagnostics.map((item) => ({ ...item })),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-archived-chats",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "DeepSeek Harness 会话档案:按工作区浏览和全文搜索归档聊天,原生只读预览对话、工具活动和已存储图片,管理标签备注与 ZIP 备份恢复;保留历史版本并恢复为副本,提供可撤销回收站、空间分账、保留策略和来源与分支。所有数据留在本机。 Session Archive for DeepSeek Harness: browse and full-text search archived chats; natively preview conversations, tool activity, and stored images read-only; manage tags, notes, and ZIP backup restore; retain local History with restore-as-copy, an undoable Recycle Bin, storage accounting, retention policies, and Origins & Branches. All data stays local.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Ultronen",
|
|
@@ -77,6 +77,9 @@
|
|
|
77
77
|
"cordis.patch.yml"
|
|
78
78
|
],
|
|
79
79
|
"dsh": {
|
|
80
|
+
"engines": {
|
|
81
|
+
"dsh": ">=0.1.0-rc.7"
|
|
82
|
+
},
|
|
80
83
|
"bundle": {
|
|
81
84
|
"patch": "./cordis.patch.yml"
|
|
82
85
|
},
|