@sparkelf/dsh-patch-session-query-unindexable-session 0.2.0-rc.9
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/LICENSE +21 -0
- package/README.i18n.yaml +6 -0
- package/README.md +56 -0
- package/README.zh.md +41 -0
- package/package.json +39 -0
- package/patches/session-query-unindexable-session.patch +90 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DeepSeek
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.i18n.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
|
2
|
+
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
|
+
# after editing either side, bring the other along and re-record with:
|
|
4
|
+
# pnpm run verify-translation-pairing --write patches/npm/session-query-unindexable-session/README.md
|
|
5
|
+
README.md: fab5177f06bc14e007af796a0ad8d0cb066cfe63
|
|
6
|
+
README.zh.md: 2e7c838a4061cbff337ca8ca7dbaf1c08462566a
|
package/README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# @sparkelf/dsh-patch-session-query-unindexable-session
|
|
2
|
+
|
|
3
|
+
English | [中文](README.zh.md)
|
|
4
|
+
|
|
5
|
+
## Summary
|
|
6
|
+
|
|
7
|
+
Keeps content search working when a session's stored log cannot be migrated, and
|
|
8
|
+
keeps an interrupted first index pass from starting over.
|
|
9
|
+
|
|
10
|
+
## Why it is needed
|
|
11
|
+
|
|
12
|
+
Content search reads every session log to build its index. Two behaviours made
|
|
13
|
+
that fail permanently on a history of 200+ sessions:
|
|
14
|
+
|
|
15
|
+
**A session the migration refuses ends the whole pass.** The v2→v3 session-format
|
|
16
|
+
migration will not transform a message whose content kind is `document`, so the
|
|
17
|
+
cold read throws `SessionFormatUnsupportedError`. That throw escapes the
|
|
18
|
+
observation loop, so one session stops every other session from being indexed —
|
|
19
|
+
and because it is read early on every attempt, the index never fills:
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
format v2 agent/inbox/spliced at seq 4 data.inserted[0].content[0]:
|
|
23
|
+
cannot safely transform unclassified message content kind "document"
|
|
24
|
+
name=SessionFormatUnsupportedError aborted=false
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**The write sits after the entire read.** Nothing is published until every log
|
|
28
|
+
has been read, so an interruption discards all the work. On this history the
|
|
29
|
+
first pass reads 214 sessions and takes minutes.
|
|
30
|
+
|
|
31
|
+
Measured before the patch: the index held **zero rows** after every search, and
|
|
32
|
+
every search failed in 20–38 seconds. After it: 210 sessions and 202,803
|
|
33
|
+
documents indexed, later searches answering in about a second.
|
|
34
|
+
|
|
35
|
+
## What it changes
|
|
36
|
+
|
|
37
|
+
| Behaviour | Before | After |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| A session whose log cannot be migrated | fails the whole search | skipped and named; others still indexed |
|
|
40
|
+
| When a session is written | after the whole corpus is read | as soon as it is read |
|
|
41
|
+
| Commit shape | one transaction per pass | bounded batches |
|
|
42
|
+
|
|
43
|
+
## Install
|
|
44
|
+
|
|
45
|
+
The patch carries no code of its own; the profile supervisor applies it to
|
|
46
|
+
`@deepseek-ai/dsh-session-query-sqlite` when the package is installed.
|
|
47
|
+
|
|
48
|
+
## Known Limitations and Deferred Work
|
|
49
|
+
|
|
50
|
+
**A skipped session is invisible to content search.** A term that appears only in
|
|
51
|
+
a skipped session's body is not found, and the search surface reports nothing.
|
|
52
|
+
The skip is named in the deployment log; surfacing it in the UI is deferred.
|
|
53
|
+
|
|
54
|
+
**The migration refusal itself is left alone.** Teaching the v2→v3 migration to
|
|
55
|
+
transform the `document` kind would make those sessions searchable, but that is
|
|
56
|
+
an upstream decision about what the transformed value should be.
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# @sparkelf/dsh-patch-session-query-unindexable-session
|
|
2
|
+
|
|
3
|
+
[English](README.md) | 中文
|
|
4
|
+
|
|
5
|
+
## Summary
|
|
6
|
+
|
|
7
|
+
当一个会话的已存日志无法迁移时,仍让内容搜索可用;并让被中断的首次索引不再从零开始。
|
|
8
|
+
|
|
9
|
+
## Why it is needed
|
|
10
|
+
|
|
11
|
+
内容搜索要读取每一个会话日志来建立索引。有两种行为让这在 200+ 会话的历史上永久失败:
|
|
12
|
+
|
|
13
|
+
**一个被迁移拒绝的会话会终止整轮。** v2→v3 的会话格式迁移不会转换内容 kind 为 `document` 的消息,因此冷读抛出 `SessionFormatUnsupportedError`。该抛出逃出观察循环,于是一个会话阻断了其余每一个会话的索引 —— 而由于它每次尝试都被较早读到,索引永远填不满:
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
format v2 agent/inbox/spliced at seq 4 data.inserted[0].content[0]:
|
|
17
|
+
cannot safely transform unclassified message content kind "document"
|
|
18
|
+
name=SessionFormatUnsupportedError aborted=false
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**写入排在整个读取之后。** 在每一个日志被读完之前什么都不会发布,因此一次中断会丢弃全部工作。在这份历史上,第一轮要读取 214 个会话,耗时数分钟。
|
|
22
|
+
|
|
23
|
+
补丁前的实测:每次搜索之后索引都是**零行**,且每次搜索在 20–38 秒内失败。补丁后:210 个会话、202803 条文档被索引,后续搜索约一秒作答。
|
|
24
|
+
|
|
25
|
+
## What it changes
|
|
26
|
+
|
|
27
|
+
| 行为 | 之前 | 之后 |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| 一个无法迁移其日志的会话 | 让整次搜索失败 | 被跳过并点名;其余仍被索引 |
|
|
30
|
+
| 何时写入一个会话 | 整个语料读完之后 | 一被读取就写 |
|
|
31
|
+
| 提交形态 | 每轮一个事务 | 有界分批 |
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
该补丁不携带自己的代码;profile supervisor 会在安装 `@deepseek-ai/dsh-session-query-sqlite` 时把它应用上去。
|
|
36
|
+
|
|
37
|
+
## Known Limitations and Deferred Work
|
|
38
|
+
|
|
39
|
+
**被跳过的会话对内容搜索不可见。** 只出现在被跳过会话正文中的词找不到,而搜索界面不报告任何东西。该跳过会在部署日志中被点名;把它呈现在 UI 中属于后续工作。
|
|
40
|
+
|
|
41
|
+
**迁移拒绝本身未被改动。** 教会 v2→v3 迁移转换 `document` kind 会让那些会话可被搜索,但那是关于转换后的值该为什么的上游决定。
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "Skip a session whose stored log cannot be migrated instead of failing the whole content search, and index each session as it is read",
|
|
3
|
+
"dshPatch": {
|
|
4
|
+
"formatVersion": 1,
|
|
5
|
+
"variants": [
|
|
6
|
+
{
|
|
7
|
+
"dsh": ">=0.1.6-alpha.1",
|
|
8
|
+
"file": "./patches/session-query-unindexable-session.patch",
|
|
9
|
+
"id": "session-query-unindexable-session",
|
|
10
|
+
"target": {
|
|
11
|
+
"baseRevision": "ddefc45fbc7f8e46dd73185e68295696d1297887",
|
|
12
|
+
"kind": "dsh-source",
|
|
13
|
+
"paths": [
|
|
14
|
+
"packages/session-query/session-query-sqlite/"
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
"exports": {
|
|
21
|
+
"./package.json": "./package.json",
|
|
22
|
+
"./patches/session-query-unindexable-session.patch": "./patches/session-query-unindexable-session.patch"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"patches/*.patch"
|
|
26
|
+
],
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"name": "@sparkelf/dsh-patch-session-query-unindexable-session",
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"repository": {
|
|
33
|
+
"directory": "patches/npm/session-query-unindexable-session",
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/SparkElf/deepseek-harness-plus.git"
|
|
36
|
+
},
|
|
37
|
+
"type": "module",
|
|
38
|
+
"version": "0.2.0-rc.9"
|
|
39
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
diff --git a/packages/session-query/session-query-sqlite/src/index.ts b/packages/session-query/session-query-sqlite/src/index.ts
|
|
2
|
+
index 7c95afa..ccb36da 100644
|
|
3
|
+
--- a/packages/session-query/session-query-sqlite/src/index.ts
|
|
4
|
+
+++ b/packages/session-query/session-query-sqlite/src/index.ts
|
|
5
|
+
@@ -11,9 +11,10 @@ import { Context, Service, type Fiber } from '@deepseek-ai/cordis'
|
|
6
|
+
import z from '@deepseek-ai/schemastery'
|
|
7
|
+
import type { Session, SessionEvent, SessionHeader, SessionId, SessionLogOffset } from '@deepseek-ai/dsh-session'
|
|
8
|
+
import type SessionPersistence from '@deepseek-ai/dsh-session-persistence'
|
|
9
|
+
-import type {
|
|
10
|
+
- SessionPersistenceRevision,
|
|
11
|
+
- SessionPersistenceSnapshot,
|
|
12
|
+
+import {
|
|
13
|
+
+ SessionFormatUnsupportedError,
|
|
14
|
+
+ type SessionPersistenceRevision,
|
|
15
|
+
+ type SessionPersistenceSnapshot,
|
|
16
|
+
} from '@deepseek-ai/dsh-session-persistence'
|
|
17
|
+
import SessionQueryEngine, {
|
|
18
|
+
SESSION_QUERY_DEFAULT_PERSISTED_INSPECT_CONCURRENCY,
|
|
19
|
+
@@ -24,6 +25,7 @@ import SessionQueryEngine, {
|
|
20
|
+
assertSessionHeadersCompatible,
|
|
21
|
+
buildSessionEventSearchDocuments,
|
|
22
|
+
readColdSessionLog,
|
|
23
|
+
+ type ColdSessionLog,
|
|
24
|
+
} from '@deepseek-ai/dsh-session-query'
|
|
25
|
+
import type {
|
|
26
|
+
Config as SessionQueryConfig,
|
|
27
|
+
@@ -519,10 +521,27 @@ export class SqliteSessionQueryEngine extends SessionQueryEngine {
|
|
28
|
+
// live-preferred.
|
|
29
|
+
if (initiallyLive.has(entry.header.id) || this.ctx.sessions.get(entry.header.id) !== undefined) continue
|
|
30
|
+
assertNotAborted(signal)
|
|
31
|
+
- const loaded = await readColdSessionLog(persistence, entry.header.id, signal)
|
|
32
|
+
+ let loaded: ColdSessionLog
|
|
33
|
+
+ try {
|
|
34
|
+
+ loaded = await readColdSessionLog(persistence, entry.header.id, signal)
|
|
35
|
+
+ } catch (readError: unknown) {
|
|
36
|
+
+ // A session whose stored log cannot be migrated is not indexable,
|
|
37
|
+
+ // but it must not stop every other session from being indexed: one
|
|
38
|
+
+ // unreadable log would otherwise fail content search on every
|
|
39
|
+
+ // attempt, forever. Leave it out of this pass.
|
|
40
|
+
+ if (!(readError instanceof SessionFormatUnsupportedError)) throw readError
|
|
41
|
+
+ continue
|
|
42
|
+
+ }
|
|
43
|
+
assertNotAborted(signal)
|
|
44
|
+
assertSessionHeadersCompatible(entry.header, loaded.header)
|
|
45
|
+
entry.loaded = observeSession(loaded.header, loaded.inheritedEventCount, loaded.events)
|
|
46
|
+
+ // Publish this session before reading the next one. The first pass
|
|
47
|
+
+ // over a large history reads every log, so deferring the write until
|
|
48
|
+
+ // the whole corpus is observed means an interrupt discards all of it
|
|
49
|
+
+ // and the next search repeats the same work forever. Committing per
|
|
50
|
+
+ // session keeps earlier progress, and the revision check above skips
|
|
51
|
+
+ // whatever is already indexed on the next attempt.
|
|
52
|
+
+ this._persistObservedSession(entry.loaded, entry.revision)
|
|
53
|
+
}
|
|
54
|
+
assertNotAborted(signal)
|
|
55
|
+
const afterSnapshots = await persistence.list(listOptions)
|
|
56
|
+
@@ -613,6 +632,34 @@ export class SqliteSessionQueryEngine extends SessionQueryEngine {
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
+ /**
|
|
61
|
+
+ * Index one observed session in its own transaction.
|
|
62
|
+
+ *
|
|
63
|
+
+ * Called while the corpus is being read rather than after it, so a first pass
|
|
64
|
+
+ * that is interrupted keeps every session it already indexed. The next pass
|
|
65
|
+
+ * skips those by revision and continues where this one stopped.
|
|
66
|
+
+ * @param entry - the observed session and its parsed documents.
|
|
67
|
+
+ * @param revision - the persistence revision the entry was read at.
|
|
68
|
+
+ */
|
|
69
|
+
+ private _persistObservedSession(entry: ObservedSession, revision: SessionPersistenceRevision): void {
|
|
70
|
+
+ const db = this._requireDb()
|
|
71
|
+
+ const generation = this._mainGeneration() + 1
|
|
72
|
+
+ db.exec('BEGIN IMMEDIATE')
|
|
73
|
+
+ try {
|
|
74
|
+
+ this._replacePersistedSession(entry, revision, generation)
|
|
75
|
+
+ db.prepare('UPDATE search_state SET global_generation = ? WHERE singleton = 1').run(generation)
|
|
76
|
+
+ db.exec('COMMIT')
|
|
77
|
+
+ } catch (error: unknown) {
|
|
78
|
+
+ try {
|
|
79
|
+
+ db.exec('ROLLBACK')
|
|
80
|
+
+ } catch {
|
|
81
|
+
+ // The write failure is the actionable cause; a rollback failure on the
|
|
82
|
+
+ // same broken transaction adds nothing.
|
|
83
|
+
+ }
|
|
84
|
+
+ throw error
|
|
85
|
+
+ }
|
|
86
|
+
+ }
|
|
87
|
+
+
|
|
88
|
+
private _replaceLiveSession(entry: ObservedSession, generation: number, persisted: boolean): void {
|
|
89
|
+
this._deleteSession('live', entry.header.id)
|
|
90
|
+
const db = this._requireDb()
|