dsh-plugin-xujie-memory 0.5.0 → 0.7.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 (84) hide show
  1. package/ACCEPTANCE.md +37 -0
  2. package/BUILD-PROVENANCE.json +53 -0
  3. package/CHANGELOG.md +40 -1
  4. package/LICENSE +21 -21
  5. package/README.md +67 -17
  6. package/README_EN.md +54 -8
  7. package/cordis.patch.yml +49 -49
  8. package/lib/client.js +17219 -0
  9. package/lib/controller.js +307 -0
  10. package/lib/index.js +200 -14
  11. package/lib/typert.host.d.ts +3 -0
  12. package/lib/typert.host.js +15104 -0
  13. package/lib/typert.remote-client.d.ts +40 -0
  14. package/lib/typert.remote-client.d.ts.map +1 -0
  15. package/lib/typert.remote-client.js +15093 -0
  16. package/lib/types.d.ts +13 -0
  17. package/lib/types.js +1 -0
  18. package/package.json +49 -5
  19. package/python/specs/characters/aloof.json +14 -14
  20. package/python/specs/characters/gentle.json +14 -14
  21. package/python/specs/characters/tsundere.json +14 -14
  22. package/python/specs/debt.v1.json +31 -31
  23. package/python/specs/nickname.v1.json +16 -16
  24. package/python/specs/preference.v1.json +16 -16
  25. package/python/xujie_memory/__init__.py +123 -123
  26. package/python/xujie_memory/admission.py +211 -211
  27. package/python/xujie_memory/audit.py +377 -377
  28. package/python/xujie_memory/benchmark.py +281 -281
  29. package/python/xujie_memory/character.py +199 -199
  30. package/python/xujie_memory/console.py +41 -41
  31. package/python/xujie_memory/consolidate.py +356 -356
  32. package/python/xujie_memory/due.py +121 -121
  33. package/python/xujie_memory/emotion.py +573 -573
  34. package/python/xujie_memory/engine.py +1333 -1312
  35. package/python/xujie_memory/errors.py +98 -98
  36. package/python/xujie_memory/importance.py +104 -104
  37. package/python/xujie_memory/judge.py +350 -350
  38. package/python/xujie_memory/management.py +203 -0
  39. package/python/xujie_memory/memory.py +71 -10
  40. package/python/xujie_memory/orchestrator.py +690 -666
  41. package/python/xujie_memory/phrasing.py +292 -292
  42. package/python/xujie_memory/privacy.py +105 -0
  43. package/python/xujie_memory/schema.sql +492 -480
  44. package/python/xujie_memory/server.py +76 -0
  45. package/python/xujie_memory/specs.py +170 -170
  46. package/python/xujie_memory/summary.py +238 -238
  47. package/scripts/build.mjs +19 -0
  48. package/scripts/prepack.mjs +9 -0
  49. package/scripts/test-all.mjs +9 -0
  50. package/scripts/test-node.mjs +10 -0
  51. package/src/client/MemoryPanel.tsx +536 -0
  52. package/src/client/NeuralField.tsx +88 -0
  53. package/src/client/css.d.ts +4 -0
  54. package/src/client/icons.tsx +28 -0
  55. package/src/client/index.tsx +59 -0
  56. package/src/client/memory-adapter.ts +67 -0
  57. package/src/client/model.ts +79 -0
  58. package/src/client/neural/engine.ts +999 -0
  59. package/src/client/neural/morphology.ts +219 -0
  60. package/src/client/panel.css +337 -0
  61. package/src/client/types.ts +6 -0
  62. package/src/controller.ts +88 -0
  63. package/src/generated/typert.host.js +449 -0
  64. package/src/generated/typert.remote-client.js +438 -0
  65. package/src/index.ts +2 -0
  66. package/src/types.ts +13 -0
  67. package/test/cordis-fixture.mjs +5 -0
  68. package/test/management_regression.py +187 -0
  69. package/test/node-regression.entry.mjs +52 -0
  70. package/vendor/DSH-LICENSE.txt +21 -0
  71. package/vendor/THIRD-PARTY.md +6 -0
  72. package/vendor/dsh-protocol/index.ts +484 -0
  73. package/vendor/dsh-protocol/json-value.ts +53 -0
  74. package/vendor/dsh-protocol/owned-value.ts +39 -0
  75. package/vendor/dsh-protocol/principal.ts +17 -0
  76. package/vendor/dsh-protocol/remote-access.ts +38 -0
  77. package/vendor/dsh-protocol/remote-error.ts +49 -0
  78. package/vendor/dsh-protocol/remote-info.ts +161 -0
  79. package/vendor/dsh-protocol/types.ts +939 -0
  80. package/vendor/licenses/jridgewell-gen-mapping-MIT.txt +19 -0
  81. package/vendor/licenses/jridgewell-resolve-uri-MIT.txt +19 -0
  82. package/vendor/licenses/jridgewell-sourcemap-codec-MIT.txt +19 -0
  83. package/vendor/licenses/jridgewell-trace-mapping-MIT.txt +19 -0
  84. package/vendor/licenses/zod-MIT.txt +21 -0
@@ -1,238 +1,238 @@
1
- """摘要:合并、校验、版本化。
2
-
3
- 设计文档 5.8 / 12.2:
4
- 合并创建新产物;旧摘要可归档,但依赖关系要保留。
5
- 摘要不能成为精确数量计算、权限裁决和冲突解决的唯一依据。
6
-
7
- 这里的默认实现是**抽取式**(取每条痕迹的首句拼接),不调用模型。
8
- 接模型只需替换 `Summarizer`,其余逻辑不变。
9
- """
10
-
11
- from __future__ import annotations
12
-
13
- import json
14
- from dataclasses import dataclass
15
- from typing import Callable, Protocol, Sequence
16
-
17
- from .engine import canonical_json, digest
18
-
19
- # 省略校验用的词表。宁可误报,不可漏报——摘要丢一个"不"字,语义就反了。
20
- # 但"别"歧义太大("别忘""别人""特别"都不是否定),所以不收。
21
- NEGATION = ("不", "没", "未", "甭")
22
-
23
- # 这些搭配里虽然有否定字,但不构成否定命题,先剔除再检测。
24
- SAFE_PHRASES = ("不过", "不管", "不仅", "不如", "不少", "无论", "没事", "没关系")
25
- CONDITION = ("要是", "如果", "假如", "倘若", "万一", "除非")
26
- PLAN = ("明天", "下次", "回头", "待会", "以后", "等我", "将来")
27
-
28
-
29
- class Summarizer(Protocol):
30
- def __call__(self, texts: Sequence[str]) -> str:
31
- ...
32
-
33
-
34
- def extractive(texts: Sequence[str], max_chars: int = 200) -> str:
35
- """默认实现:抽取式。不生成新内容,只挑原文片段。
36
-
37
- 好处:永远不会引入原文没有的说法。
38
- 代价:长对话里信息覆盖差。接模型时替换这个函数即可。
39
- """
40
- parts: list[str] = []
41
- used = 0
42
- for t in texts:
43
- head = t.strip().split("。")[0]
44
- if not head:
45
- continue
46
- if used + len(head) > max_chars:
47
- break
48
- parts.append(head)
49
- used += len(head)
50
- return ";".join(parts)
51
-
52
-
53
- @dataclass
54
- class SummaryResult:
55
- summary_id: str
56
- text: str
57
- input_refs: list[str]
58
- validation_status: str # ok / failed
59
- warnings: list[str]
60
-
61
-
62
- def validate_omissions(texts: Sequence[str], summary: str) -> list[str]:
63
- """检查摘要是否丢掉了会改变语义的成分(12.2)。
64
-
65
- 这是启发式,不是证明。它只能发现「原文有、摘要没有」的显性丢失,
66
- 发现不了「换了说法但意思变了」。
67
- """
68
- warnings: list[str] = []
69
- source = "".join(texts)
70
- for phrase in SAFE_PHRASES:
71
- source = source.replace(phrase, " ")
72
-
73
- for label, words in (("negation", NEGATION), ("condition", CONDITION), ("plan", PLAN)):
74
- src_hits = {w for w in words if w in source}
75
- if not src_hits:
76
- continue
77
- miss = [w for w in src_hits if w not in summary]
78
- if miss:
79
- warnings.append(f"{label}_lost:{','.join(sorted(miss))}")
80
-
81
- # 数字:摘要里出现的数字必须能在原文里找到
82
- import re
83
- src_nums = set(re.findall(r"\d+", source))
84
- sum_nums = set(re.findall(r"\d+", summary))
85
- extra = sum_nums - src_nums
86
- if extra:
87
- warnings.append(f"numbers_not_in_source:{','.join(sorted(extra))}")
88
-
89
- return warnings
90
-
91
-
92
- class SummaryStore:
93
- def __init__(self, engine):
94
- self.engine = engine
95
-
96
- # ------------------------------------------------------------- 生成
97
- def create(
98
- self,
99
- scope_id: str,
100
- trace_ids: Sequence[str],
101
- summarizer: Summarizer = extractive,
102
- generator_version: str = "extractive-v1",
103
- coverage: dict | None = None,
104
- ) -> SummaryResult:
105
- texts: list[str] = []
106
- for tid in trace_ids:
107
- t = self.engine.get_trace(tid)
108
- if t is None or t["erased_at"]:
109
- continue
110
- texts.append(t["content"])
111
-
112
- text = summarizer(texts)
113
- warnings = validate_omissions(texts, text)
114
- status = "failed" if any(w.startswith("negation_lost") for w in warnings) else "ok"
115
-
116
- summary_id = "sm_" + digest(scope_id, canonical_json(list(trace_ids)), self.engine.now())[:24]
117
- self.engine._conn().execute(
118
- "INSERT INTO summaries(summary_id, scope_id, input_refs_json, text,"
119
- " generator_version, coverage_json, omissions_json, validation_status,"
120
- " lifecycle_state, created_at) VALUES (?,?,?,?,?,?,?,?,?,?)",
121
- (
122
- summary_id, scope_id, canonical_json(list(trace_ids)), text,
123
- generator_version, canonical_json(coverage or {}),
124
- canonical_json(warnings), status, "active", self.engine.now(),
125
- ),
126
- )
127
- return SummaryResult(summary_id, text, list(trace_ids), status, warnings)
128
-
129
- # ------------------------------------------------------------- 合并
130
- def merge(
131
- self,
132
- scope_id: str,
133
- summary_ids: Sequence[str],
134
- summarizer: Summarizer = extractive,
135
- generator_version: str = "extractive-v1",
136
- ) -> SummaryResult:
137
- """合并多个摘要:生成新产物,旧产物标 merged,不覆盖(12.2)。"""
138
- conn = self.engine._conn()
139
- # 顺序按时间正序,不按 IN 列表顺序,也不按数据库返回顺序。
140
- #
141
- # 摘要是给人读的:「还你三斤;再还两斤;免一斤吧」和
142
- # 「免一斤吧;再还两斤;还你三斤」是两段不同的话。
143
- # 而 IN 查询的返回顺序**不保证**(这里实测是倒的),
144
- # 所以必须显式 ORDER BY,并用 summary_id 兜底保证可复现(I2)。
145
- rows = conn.execute(
146
- f"SELECT * FROM summaries WHERE summary_id IN ({','.join('?' * len(summary_ids))})"
147
- f" ORDER BY created_at ASC, summary_id ASC",
148
- tuple(summary_ids),
149
- ).fetchall()
150
- if not rows:
151
- raise ValueError("no summaries to merge")
152
-
153
- texts = [r["text"] for r in rows]
154
- inputs: list[str] = []
155
- for r in rows:
156
- inputs.extend(json.loads(r["input_refs_json"]))
157
-
158
- text = summarizer(texts)
159
- warnings = validate_omissions(texts, text)
160
- if warnings:
161
- # 校验失败保留旧产物,不能因为「更短」就替换(12.2)
162
- return SummaryResult("", text, inputs, "failed", warnings)
163
-
164
- result = self.create(scope_id, inputs, summarizer, generator_version,
165
- coverage={"merged_from": list(summary_ids)})
166
- conn.execute(
167
- f"UPDATE summaries SET lifecycle_state='merged'"
168
- f" WHERE summary_id IN ({','.join('?' * len(summary_ids))})",
169
- tuple(summary_ids),
170
- )
171
- return result
172
-
173
- # ------------------------------------------------------------- 读
174
- def active(self, scope_id: str) -> list[dict]:
175
- # summary_id 是兜底排序键:同一秒内产生的多条摘要,光按 created_at
176
- # 排出来的顺序由存储引擎决定——这次是 A、下次可能是 B。
177
- # 而整合周期要按这个顺序分组合并,顺序不稳就等于**不幂等**(I2)。
178
- rows = self.engine._conn().execute(
179
- "SELECT * FROM summaries WHERE scope_id=? AND lifecycle_state='active'"
180
- " ORDER BY created_at DESC, summary_id ASC",
181
- (scope_id,),
182
- ).fetchall()
183
- return [dict(r) for r in rows]
184
-
185
- def get(self, summary_id: str) -> dict | None:
186
- row = self.engine._conn().execute(
187
- "SELECT * FROM summaries WHERE summary_id=?", (summary_id,)
188
- ).fetchone()
189
- return dict(row) if row else None
190
-
191
-
192
- def expire_states(engine, scope_id: str, now: str | None = None) -> list[str]:
193
- """按规格的 validity 把过期状态转 stale(12.3)。
194
-
195
- 注意:这只改 status,不改 value。历史上仍可查,只是不再充当「现在」。
196
- """
197
- now = now or engine.now()
198
- stale: list[str] = []
199
- conn = engine._conn()
200
-
201
- for spec in engine.registry.all():
202
- if spec.validity != "ttl" or not spec.ttl_seconds:
203
- continue
204
- rows = conn.execute(
205
- "SELECT state_key, status FROM states WHERE scope_id=? AND status IN ('known','stale')",
206
- (scope_id,),
207
- ).fetchall()
208
- for row in rows:
209
- if relation_of_key(row["state_key"]) != spec.relation:
210
- continue
211
- last = conn.execute(
212
- "SELECT MAX(effective_at) FROM events WHERE state_key=? AND void=0",
213
- (row["state_key"],),
214
- ).fetchone()[0]
215
- if last is None:
216
- continue
217
- from datetime import datetime, timezone
218
- try:
219
- ts = datetime.fromisoformat(last)
220
- except ValueError:
221
- continue
222
- if ts.tzinfo is None:
223
- ts = ts.replace(tzinfo=timezone.utc)
224
- cur = datetime.fromisoformat(now)
225
- if cur.tzinfo is None:
226
- cur = cur.replace(tzinfo=timezone.utc)
227
- if (cur - ts).total_seconds() > spec.ttl_seconds:
228
- conn.execute(
229
- "UPDATE states SET status='stale' WHERE state_key=?",
230
- (row["state_key"],),
231
- )
232
- stale.append(row["state_key"])
233
- return stale
234
-
235
-
236
- def relation_of_key(state_key: str) -> str:
237
- parts = state_key.split("|")
238
- return parts[2] if len(parts) >= 3 else ""
1
+ """摘要:合并、校验、版本化。
2
+
3
+ 设计文档 5.8 / 12.2:
4
+ 合并创建新产物;旧摘要可归档,但依赖关系要保留。
5
+ 摘要不能成为精确数量计算、权限裁决和冲突解决的唯一依据。
6
+
7
+ 这里的默认实现是**抽取式**(取每条痕迹的首句拼接),不调用模型。
8
+ 接模型只需替换 `Summarizer`,其余逻辑不变。
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ from dataclasses import dataclass
15
+ from typing import Callable, Protocol, Sequence
16
+
17
+ from .engine import canonical_json, digest
18
+
19
+ # 省略校验用的词表。宁可误报,不可漏报——摘要丢一个"不"字,语义就反了。
20
+ # 但"别"歧义太大("别忘""别人""特别"都不是否定),所以不收。
21
+ NEGATION = ("不", "没", "未", "甭")
22
+
23
+ # 这些搭配里虽然有否定字,但不构成否定命题,先剔除再检测。
24
+ SAFE_PHRASES = ("不过", "不管", "不仅", "不如", "不少", "无论", "没事", "没关系")
25
+ CONDITION = ("要是", "如果", "假如", "倘若", "万一", "除非")
26
+ PLAN = ("明天", "下次", "回头", "待会", "以后", "等我", "将来")
27
+
28
+
29
+ class Summarizer(Protocol):
30
+ def __call__(self, texts: Sequence[str]) -> str:
31
+ ...
32
+
33
+
34
+ def extractive(texts: Sequence[str], max_chars: int = 200) -> str:
35
+ """默认实现:抽取式。不生成新内容,只挑原文片段。
36
+
37
+ 好处:永远不会引入原文没有的说法。
38
+ 代价:长对话里信息覆盖差。接模型时替换这个函数即可。
39
+ """
40
+ parts: list[str] = []
41
+ used = 0
42
+ for t in texts:
43
+ head = t.strip().split("。")[0]
44
+ if not head:
45
+ continue
46
+ if used + len(head) > max_chars:
47
+ break
48
+ parts.append(head)
49
+ used += len(head)
50
+ return ";".join(parts)
51
+
52
+
53
+ @dataclass
54
+ class SummaryResult:
55
+ summary_id: str
56
+ text: str
57
+ input_refs: list[str]
58
+ validation_status: str # ok / failed
59
+ warnings: list[str]
60
+
61
+
62
+ def validate_omissions(texts: Sequence[str], summary: str) -> list[str]:
63
+ """检查摘要是否丢掉了会改变语义的成分(12.2)。
64
+
65
+ 这是启发式,不是证明。它只能发现「原文有、摘要没有」的显性丢失,
66
+ 发现不了「换了说法但意思变了」。
67
+ """
68
+ warnings: list[str] = []
69
+ source = "".join(texts)
70
+ for phrase in SAFE_PHRASES:
71
+ source = source.replace(phrase, " ")
72
+
73
+ for label, words in (("negation", NEGATION), ("condition", CONDITION), ("plan", PLAN)):
74
+ src_hits = {w for w in words if w in source}
75
+ if not src_hits:
76
+ continue
77
+ miss = [w for w in src_hits if w not in summary]
78
+ if miss:
79
+ warnings.append(f"{label}_lost:{','.join(sorted(miss))}")
80
+
81
+ # 数字:摘要里出现的数字必须能在原文里找到
82
+ import re
83
+ src_nums = set(re.findall(r"\d+", source))
84
+ sum_nums = set(re.findall(r"\d+", summary))
85
+ extra = sum_nums - src_nums
86
+ if extra:
87
+ warnings.append(f"numbers_not_in_source:{','.join(sorted(extra))}")
88
+
89
+ return warnings
90
+
91
+
92
+ class SummaryStore:
93
+ def __init__(self, engine):
94
+ self.engine = engine
95
+
96
+ # ------------------------------------------------------------- 生成
97
+ def create(
98
+ self,
99
+ scope_id: str,
100
+ trace_ids: Sequence[str],
101
+ summarizer: Summarizer = extractive,
102
+ generator_version: str = "extractive-v1",
103
+ coverage: dict | None = None,
104
+ ) -> SummaryResult:
105
+ texts: list[str] = []
106
+ for tid in trace_ids:
107
+ t = self.engine.get_trace(tid)
108
+ if t is None or t["erased_at"]:
109
+ continue
110
+ texts.append(t["content"])
111
+
112
+ text = summarizer(texts)
113
+ warnings = validate_omissions(texts, text)
114
+ status = "failed" if any(w.startswith("negation_lost") for w in warnings) else "ok"
115
+
116
+ summary_id = "sm_" + digest(scope_id, canonical_json(list(trace_ids)), self.engine.now())[:24]
117
+ self.engine._conn().execute(
118
+ "INSERT INTO summaries(summary_id, scope_id, input_refs_json, text,"
119
+ " generator_version, coverage_json, omissions_json, validation_status,"
120
+ " lifecycle_state, created_at) VALUES (?,?,?,?,?,?,?,?,?,?)",
121
+ (
122
+ summary_id, scope_id, canonical_json(list(trace_ids)), text,
123
+ generator_version, canonical_json(coverage or {}),
124
+ canonical_json(warnings), status, "active", self.engine.now(),
125
+ ),
126
+ )
127
+ return SummaryResult(summary_id, text, list(trace_ids), status, warnings)
128
+
129
+ # ------------------------------------------------------------- 合并
130
+ def merge(
131
+ self,
132
+ scope_id: str,
133
+ summary_ids: Sequence[str],
134
+ summarizer: Summarizer = extractive,
135
+ generator_version: str = "extractive-v1",
136
+ ) -> SummaryResult:
137
+ """合并多个摘要:生成新产物,旧产物标 merged,不覆盖(12.2)。"""
138
+ conn = self.engine._conn()
139
+ # 顺序按时间正序,不按 IN 列表顺序,也不按数据库返回顺序。
140
+ #
141
+ # 摘要是给人读的:「还你三斤;再还两斤;免一斤吧」和
142
+ # 「免一斤吧;再还两斤;还你三斤」是两段不同的话。
143
+ # 而 IN 查询的返回顺序**不保证**(这里实测是倒的),
144
+ # 所以必须显式 ORDER BY,并用 summary_id 兜底保证可复现(I2)。
145
+ rows = conn.execute(
146
+ f"SELECT * FROM summaries WHERE summary_id IN ({','.join('?' * len(summary_ids))})"
147
+ f" ORDER BY created_at ASC, summary_id ASC",
148
+ tuple(summary_ids),
149
+ ).fetchall()
150
+ if not rows:
151
+ raise ValueError("no summaries to merge")
152
+
153
+ texts = [r["text"] for r in rows]
154
+ inputs: list[str] = []
155
+ for r in rows:
156
+ inputs.extend(json.loads(r["input_refs_json"]))
157
+
158
+ text = summarizer(texts)
159
+ warnings = validate_omissions(texts, text)
160
+ if warnings:
161
+ # 校验失败保留旧产物,不能因为「更短」就替换(12.2)
162
+ return SummaryResult("", text, inputs, "failed", warnings)
163
+
164
+ result = self.create(scope_id, inputs, summarizer, generator_version,
165
+ coverage={"merged_from": list(summary_ids)})
166
+ conn.execute(
167
+ f"UPDATE summaries SET lifecycle_state='merged'"
168
+ f" WHERE summary_id IN ({','.join('?' * len(summary_ids))})",
169
+ tuple(summary_ids),
170
+ )
171
+ return result
172
+
173
+ # ------------------------------------------------------------- 读
174
+ def active(self, scope_id: str) -> list[dict]:
175
+ # summary_id 是兜底排序键:同一秒内产生的多条摘要,光按 created_at
176
+ # 排出来的顺序由存储引擎决定——这次是 A、下次可能是 B。
177
+ # 而整合周期要按这个顺序分组合并,顺序不稳就等于**不幂等**(I2)。
178
+ rows = self.engine._conn().execute(
179
+ "SELECT * FROM summaries WHERE scope_id=? AND lifecycle_state='active'"
180
+ " ORDER BY created_at DESC, summary_id ASC",
181
+ (scope_id,),
182
+ ).fetchall()
183
+ return [dict(r) for r in rows]
184
+
185
+ def get(self, summary_id: str) -> dict | None:
186
+ row = self.engine._conn().execute(
187
+ "SELECT * FROM summaries WHERE summary_id=?", (summary_id,)
188
+ ).fetchone()
189
+ return dict(row) if row else None
190
+
191
+
192
+ def expire_states(engine, scope_id: str, now: str | None = None) -> list[str]:
193
+ """按规格的 validity 把过期状态转 stale(12.3)。
194
+
195
+ 注意:这只改 status,不改 value。历史上仍可查,只是不再充当「现在」。
196
+ """
197
+ now = now or engine.now()
198
+ stale: list[str] = []
199
+ conn = engine._conn()
200
+
201
+ for spec in engine.registry.all():
202
+ if spec.validity != "ttl" or not spec.ttl_seconds:
203
+ continue
204
+ rows = conn.execute(
205
+ "SELECT state_key, status FROM states WHERE scope_id=? AND status IN ('known','stale')",
206
+ (scope_id,),
207
+ ).fetchall()
208
+ for row in rows:
209
+ if relation_of_key(row["state_key"]) != spec.relation:
210
+ continue
211
+ last = conn.execute(
212
+ "SELECT MAX(effective_at) FROM events WHERE state_key=? AND void=0",
213
+ (row["state_key"],),
214
+ ).fetchone()[0]
215
+ if last is None:
216
+ continue
217
+ from datetime import datetime, timezone
218
+ try:
219
+ ts = datetime.fromisoformat(last)
220
+ except ValueError:
221
+ continue
222
+ if ts.tzinfo is None:
223
+ ts = ts.replace(tzinfo=timezone.utc)
224
+ cur = datetime.fromisoformat(now)
225
+ if cur.tzinfo is None:
226
+ cur = cur.replace(tzinfo=timezone.utc)
227
+ if (cur - ts).total_seconds() > spec.ttl_seconds:
228
+ conn.execute(
229
+ "UPDATE states SET status='stale' WHERE state_key=?",
230
+ (row["state_key"],),
231
+ )
232
+ stale.append(row["state_key"])
233
+ return stale
234
+
235
+
236
+ def relation_of_key(state_key: str) -> str:
237
+ parts = state_key.split("|")
238
+ return parts[2] if len(parts) >= 3 else ""
@@ -0,0 +1,19 @@
1
+ import { build } from 'esbuild'
2
+ import { readFileSync, writeFileSync } from 'node:fs'
3
+ import { resolve } from 'node:path'
4
+ const root = resolve(import.meta.dirname, '..')
5
+ const alias = {
6
+ '@deepseek-ai/dsh-typert-protocol/remote-info': resolve(root, 'vendor/dsh-protocol/remote-info.ts'),
7
+ '@deepseek-ai/dsh-typert-protocol': resolve(root, 'vendor/dsh-protocol/index.ts'),
8
+ }
9
+ const common = { bundle:true,format:'esm',target:'es2022',logLevel:'warning',alias }
10
+ await build({...common,entryPoints:[resolve(root,'src/controller.ts')],platform:'node',external:['@deepseek-ai/cordis'],outfile:resolve(root,'lib/controller.js')})
11
+ for (const name of ['typert.host','typert.remote-client']) {
12
+ await build({...common,entryPoints:[resolve(root,`src/generated/${name}.js`)],platform:'neutral',outfile:resolve(root,`lib/${name}.js`)})
13
+ }
14
+ writeFileSync(resolve(root,'lib/types.d.ts'),readFileSync(resolve(root,'src/types.ts')))
15
+ writeFileSync(resolve(root,'lib/types.js'),'export {};\n')
16
+ if (process.argv.includes('--host-only')) process.exit(0)
17
+ const manifest = JSON.parse(readFileSync(resolve(root,'package.json'),'utf8'))
18
+ await build({...common,entryPoints:[resolve(root,'src/client/index.tsx')],platform:'browser',format:'cjs',external:['react','react/jsx-runtime','react-dom','react-dom/client','@deepseek-ai/cordis'],jsx:'automatic',outfile:resolve(root,'lib/client.js'),loader:{'.css':'text'},banner:{js:`window.__ModuleLoader__.load({id:${JSON.stringify(manifest.name)},factory:(require)=>{var module={exports:{}};var exports=module.exports;`},footer:{js:'return module.exports;}});'}})
19
+ console.log('Built host controller, strict contracts and DSH client factory.')
@@ -0,0 +1,9 @@
1
+ import {readdirSync,rmSync,statSync,existsSync,readFileSync} from 'node:fs'
2
+ import {resolve,join} from 'node:path'
3
+ const root=resolve(import.meta.dirname,'..')
4
+ function clean(dir){for(const entry of readdirSync(dir,{withFileTypes:true})){const p=join(dir,entry.name);if(entry.isDirectory()){if(entry.name==='__pycache__')rmSync(p,{recursive:true,force:true});else clean(p)}else if(entry.name.endsWith('.pyc'))rmSync(p)}}
5
+ clean(join(root,'python'))
6
+ for(const name of ['lib/index.js','lib/controller.js','lib/client.js','lib/typert.host.js','lib/typert.remote-client.js','python/xujie_memory/management.py','ACCEPTANCE.md'])if(!existsSync(join(root,name)))throw new Error('Missing release artifact: '+name)
7
+ const m=JSON.parse(readFileSync(join(root,'package.json'),'utf8'))
8
+ if(m.version!=='0.7.0'||m.engines.dsh!=='0.2.1-rc.5')throw new Error('Unreviewed package/host version')
9
+ console.log('Release artifacts verified; Python bytecode caches excluded.')
@@ -0,0 +1,9 @@
1
+ import {spawnSync} from 'node:child_process'
2
+ import {resolve} from 'node:path'
3
+ const root=resolve(import.meta.dirname,'..')
4
+ const python=process.env.PYTHON ?? (process.platform==='win32'?resolve(root,'runtime/win32-x64/python.exe'):'python3')
5
+ for (const [cmd,args] of [[python,['test/management_regression.py']],[process.execPath,['scripts/test-node.mjs']]]) {
6
+ const r=spawnSync(cmd,args,{cwd:root,stdio:'inherit',env:{...process.env,PYTHON:python}})
7
+ if(r.error)throw r.error
8
+ if(r.status!==0)process.exit(r.status??1)
9
+ }
@@ -0,0 +1,10 @@
1
+ import {build} from 'esbuild'
2
+ import {spawnSync} from 'node:child_process'
3
+ import {mkdtempSync,rmSync} from 'node:fs'
4
+ import {tmpdir} from 'node:os'
5
+ import {join,resolve} from 'node:path'
6
+ const root=resolve(import.meta.dirname,'..'),dir=mkdtempSync(join(tmpdir(),'xujie-test-build-')),outfile=join(dir,'test.mjs')
7
+ try{
8
+ await build({entryPoints:[join(root,'test/node-regression.entry.mjs')],bundle:true,platform:'node',format:'esm',target:'node22',outfile,external:['node:*'],alias:{'@deepseek-ai/cordis':join(root,'test/cordis-fixture.mjs')},logLevel:'warning'})
9
+ const r=spawnSync(process.execPath,[outfile],{cwd:root,stdio:'inherit',env:process.env});process.exitCode=r.status??1
10
+ }finally{rmSync(dir,{recursive:true,force:true})}