@arcfold/dsh 0.0.0-stage → 0.4.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.
- package/LICENSE +21 -0
- package/README.md +8 -2
- package/THIRD_PARTY_NOTICES.md +11 -0
- package/cordis.patch.yml +138 -0
- package/package.json +70 -4
- package/patches/deepseek-0.2.2-rc.1-transport.json +65 -0
- package/patches/deepseek-0.2.2-rc.1-transport.patch +61 -0
- package/presets/arcfold.patch.yml +110 -0
- package/runtime/README.md +19 -0
- package/runtime/bootstrap.json +15 -0
- package/runtime/desktop-python.tar.gz +0 -0
- package/runtime/host/package.json +11 -0
- package/runtime/host/pnpm-workspace.yaml +9 -0
- package/runtime/initial-policy.json +94 -0
- package/runtime/profile/pnpm-workspace.yaml +7 -0
- package/scripts/exact-package-patch.mjs +106 -0
- package/scripts/patch-deepseek-transport.mjs +14 -0
- package/src/agent.mjs +56 -0
- package/src/browse-scope.mjs +14 -0
- package/src/budget.mjs +369 -0
- package/src/business.mjs +334 -0
- package/src/context-material.mjs +119 -0
- package/src/deepseek-budget-plugin.mjs +58 -0
- package/src/deepseek-provider.mjs +57 -0
- package/src/deepseek-receipt.mjs +97 -0
- package/src/deepseek-transport.mjs +97 -0
- package/src/desktop/adapter.mjs +16 -0
- package/src/desktop/bootstrap.mjs +84 -0
- package/src/desktop/configuration.mjs +117 -0
- package/src/desktop/provider.mjs +46 -0
- package/src/desktop/python-runtime.mjs +105 -0
- package/src/desktop/setup-service.mjs +81 -0
- package/src/desktop/vendor/LICENSE.deepseek +21 -0
- package/src/desktop/vendor/NOTICE.md +5 -0
- package/src/desktop/vendor/deepseek-0.2.2-rc.1.mjs +2919 -0
- package/src/evidence-ownership.mjs +51 -0
- package/src/evidence.mjs +35 -0
- package/src/goal-context.mjs +52 -0
- package/src/goal-execution-tool.mjs +74 -0
- package/src/goal-execution.mjs +333 -0
- package/src/goal-tool.mjs +56 -0
- package/src/goal-work-control.mjs +63 -0
- package/src/goal-work.mjs +257 -0
- package/src/goals.mjs +578 -0
- package/src/importer.mjs +298 -0
- package/src/index.mjs +53 -0
- package/src/learning-contracts.mjs +220 -0
- package/src/learning-defaults.mjs +98 -0
- package/src/learning-goals.mjs +165 -0
- package/src/learning-plugin.mjs +28 -0
- package/src/learning-runner.mjs +275 -0
- package/src/learning.mjs +534 -0
- package/src/legacy-export.mjs +332 -0
- package/src/legacy-import-source.mjs +356 -0
- package/src/metered-provider-host.mjs +101 -0
- package/src/metered-transport.mjs +435 -0
- package/src/method-context.mjs +57 -0
- package/src/method-lifecycle.mjs +34 -0
- package/src/method-tool.mjs +61 -0
- package/src/methods.mjs +425 -0
- package/src/model-selection.mjs +41 -0
- package/src/native-context-call.mjs +88 -0
- package/src/native-continuation.mjs +217 -0
- package/src/native-request-authority.mjs +90 -0
- package/src/openai-models.mjs +4 -0
- package/src/openai-receipt.mjs +98 -0
- package/src/openai-transport.mjs +167 -0
- package/src/openai-usage.mjs +194 -0
- package/src/opportunities.mjs +295 -0
- package/src/preparation-artifacts.mjs +213 -0
- package/src/preparation-plugin.mjs +290 -0
- package/src/runtime-command.mjs +264 -0
- package/src/runtime-config.mjs +417 -0
- package/src/runtime-guard.mjs +284 -0
- package/src/runtime-host.mjs +212 -0
- package/src/runtime-init.mjs +278 -0
- package/src/runtime-installation.mjs +518 -0
- package/src/runtime-lifecycle.mjs +227 -0
- package/src/runtime-model-settings.mjs +111 -0
- package/src/runtime-readiness.mjs +285 -0
- package/src/runtime-workspace.mjs +18 -0
- package/src/session-bridge.mjs +365 -0
- package/src/store.mjs +394 -0
- package/src/tree/LICENSE +21 -0
- package/src/tree/attribution.mjs +120 -0
- package/src/tree/clusters.mjs +61 -0
- package/src/tree/consolidation.mjs +103 -0
- package/src/tree/context.mjs +211 -0
- package/src/tree/contracts.mjs +142 -0
- package/src/tree/experience.mjs +140 -0
- package/src/tree/experiment-budget.mjs +84 -0
- package/src/tree/hooks.mjs +254 -0
- package/src/tree/host-context.mjs +107 -0
- package/src/tree/host-process.mjs +288 -0
- package/src/tree/host.mjs +37 -0
- package/src/tree/improvement.mjs +106 -0
- package/src/tree/items.mjs +294 -0
- package/src/tree/jobs.mjs +184 -0
- package/src/tree/learning.mjs +202 -0
- package/src/tree/metrics.mjs +203 -0
- package/src/tree/model-owner.mjs +254 -0
- package/src/tree/observations.mjs +73 -0
- package/src/tree/prompts.mjs +500 -0
- package/src/tree/reports.mjs +67 -0
- package/src/tree/repository.mjs +236 -0
- package/src/tree/runtime.mjs +745 -0
- package/src/tree/script-writer.mjs +149 -0
- package/src/tree/scripts.mjs +208 -0
- package/src/tree/signals.mjs +118 -0
- package/src/tree/snapshots.mjs +379 -0
- package/src/tree/token-counter.mjs +75 -0
- package/src/tree/token-counter.py +37 -0
- package/src/tree/tokenizer-resources/LICENSE +21 -0
- package/src/tree/tokenizer-resources/README.md +31 -0
- package/src/tree/tokenizer-resources/deepseek_recipe-0.2.0-cp310-abi3-macosx_11_0_arm64.whl +0 -0
- package/src/tree/tokenizer-resources/manifest.json +61 -0
- package/src/tree/tokenizer-resources/native.pyi +1042 -0
- package/src/tree/tokenizer-resources/tokenizer-NOTICE.md +26 -0
- package/src/tree/tokenizer-resources/tokenizer-v4-NOTICE.md +27 -0
- package/src/tree/tokenizer-resources/tokenizer-v4.json +267359 -0
- package/src/tree/tokenizer-resources/tokenizer.json +267359 -0
- package/src/tree/tools.mjs +99 -0
- package/src/tree/tree.mjs +202 -0
- package/src/tree/ui.mjs +285 -0
- package/src/tree/verification.mjs +143 -0
- package/src/understandings.mjs +309 -0
- package/src/web-budget-recovery.mjs +181 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
// User control over business knowledge. Native DSH alone admits and runs input.
|
|
2
|
+
import { DomainError, canonicalJson, identity } from './store.mjs';
|
|
3
|
+
import { eventId } from './business.mjs';
|
|
4
|
+
import { hash, sourceText, sourceAvailable, dependenciesCurrent } from './evidence.mjs';
|
|
5
|
+
import { projectUnderstanding } from './learning-contracts.mjs';
|
|
6
|
+
import { invalidateDependents } from './learning-goals.mjs';
|
|
7
|
+
import { GoalService } from './goals.mjs';
|
|
8
|
+
import { browseScope, browseContext } from './browse-scope.mjs';
|
|
9
|
+
|
|
10
|
+
const fail = (code, message) => { throw new DomainError(code, message); };
|
|
11
|
+
const digest = value => hash(JSON.parse(canonicalJson(value)));
|
|
12
|
+
const copy = value => JSON.parse(canonicalJson(value));
|
|
13
|
+
const positive = value => Number.isSafeInteger(value) && value > 0;
|
|
14
|
+
const inScope = (row, sessionId) => !row.scope?.conversationId || row.scope.conversationId === sessionId;
|
|
15
|
+
const allowedKeys = (value, keys) => {
|
|
16
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).some(key => !keys.includes(key)))
|
|
17
|
+
fail('invalid_understanding_request', '理解操作只接受规定的版本、内容和可信调用身份。');
|
|
18
|
+
};
|
|
19
|
+
const commandId = (sessionId, requestId) => 'understanding-command-' + digest([sessionId, requestId]);
|
|
20
|
+
|
|
21
|
+
// Retain missing originals as missing; never fabricate their author or text.
|
|
22
|
+
function sourcesFor(store, record) {
|
|
23
|
+
const ids = new Set(), seen = new Set();
|
|
24
|
+
function visit(row) {
|
|
25
|
+
if (!row || seen.has(row.id)) return;
|
|
26
|
+
seen.add(row.id);
|
|
27
|
+
for (const id of row.sourceIds || []) ids.add(id);
|
|
28
|
+
for (const ref of row.dependencies || []) {
|
|
29
|
+
const kind = ref.kind || ['events', 'understandings', 'methods'].find(name => store.get(name, ref.id));
|
|
30
|
+
if (kind === 'events') ids.add(ref.id);
|
|
31
|
+
else if (['understandings', 'methods'].includes(kind)) visit(store.get(kind, ref.id));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
visit(record);
|
|
35
|
+
return [...ids].map(id => {
|
|
36
|
+
const source = store.get('events', id), control = store.get('source_controls', id);
|
|
37
|
+
return { id, revision: source?.revision ?? null, available: Boolean(source && sourceAvailable(store, source)),
|
|
38
|
+
missing: !source, role: source?.role ?? null, author: source?.author ?? source?.role ?? null,
|
|
39
|
+
content: source ? sourceText(source) : null, occurredAt: source?.occurredAt ?? null,
|
|
40
|
+
provenance: source?.provenance ?? null, control: control ?? null,
|
|
41
|
+
expectedControlRevision: control?.revision ?? null };
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Trusted Host supplies identity; an identity string never proves native input. */
|
|
46
|
+
export class UnderstandingService {
|
|
47
|
+
constructor({ business, goals = new GoalService({ business }) }) {
|
|
48
|
+
if (!business?.storage) fail('invalid_understanding_config', '需要业务存储。');
|
|
49
|
+
this.store = business.storage;
|
|
50
|
+
this.goals = goals;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
session(sessionId) {
|
|
54
|
+
// The authenticated Host checks the live main Agent for management.
|
|
55
|
+
// A past execution binding neither admits browsing nor proves current input.
|
|
56
|
+
identity(sessionId, 'Native main session');
|
|
57
|
+
}
|
|
58
|
+
target({ sessionId, understandingId, expectedRevision }) {
|
|
59
|
+
const row = this.store.get('understandings', understandingId);
|
|
60
|
+
if (!row || !inScope(row, sessionId)) fail('understanding_not_found', '理解不存在或不在当前交流的可见范围。');
|
|
61
|
+
if (expectedRevision !== undefined && row.revision !== expectedRevision)
|
|
62
|
+
fail('revision_conflict', '理解已经变化,请重新查看所见版本。');
|
|
63
|
+
return row;
|
|
64
|
+
}
|
|
65
|
+
view(row) {
|
|
66
|
+
const sources = sourcesFor(this.store, row);
|
|
67
|
+
const projected = projectUnderstanding(row, sources.filter(source => !source.missing));
|
|
68
|
+
const available = row.status === 'active' && dependenciesCurrent(this.store, row, this.store.currentSequence());
|
|
69
|
+
return { understanding: projected, sources, availability: { allowed: available,
|
|
70
|
+
...(!available ? { code: 'understanding_unavailable', message: '记录保留,当前不作为 Arcfold 有效背景采用。' } : {}) },
|
|
71
|
+
controls: { stopAllowed: row.status === 'active' && !row.deleted,
|
|
72
|
+
correctionAllowed: this.isCorrectable(row) },
|
|
73
|
+
historyPreserved: true, authority: 'provenance and eligibility only; not truth or action permission' };
|
|
74
|
+
}
|
|
75
|
+
list({ sessionId, viewScope, offset = 0, limit = 20 } = {}) {
|
|
76
|
+
this.session(sessionId);
|
|
77
|
+
const all = browseScope(viewScope) === 'all';
|
|
78
|
+
if (!Number.isSafeInteger(offset) || offset < 0 || !positive(limit) || limit > 100)
|
|
79
|
+
fail('invalid_pagination', '理解列表分页参数不合法。');
|
|
80
|
+
const rows = this.store.listAll('understandings').filter(row => all || inScope(row, sessionId)).reverse();
|
|
81
|
+
return { items: rows.slice(offset, offset + limit).map(row => this.browseView(row, sessionId)), total: rows.length,
|
|
82
|
+
nextOffset: offset + limit < rows.length ? offset + limit : null };
|
|
83
|
+
}
|
|
84
|
+
browseView(row, sessionId) {
|
|
85
|
+
const detail = this.view(row), browse = browseContext(row, sessionId);
|
|
86
|
+
return { ...detail, browse, controls: { ...detail.controls,
|
|
87
|
+
stopAllowed: browse.related && detail.controls.stopAllowed,
|
|
88
|
+
correctionAllowed: browse.related && detail.controls.correctionAllowed, sourceControlAllowed: browse.related } };
|
|
89
|
+
}
|
|
90
|
+
details({ sessionId, understandingId, viewScope }) {
|
|
91
|
+
this.session(sessionId); identity(understandingId, 'Understanding id');
|
|
92
|
+
const all = browseScope(viewScope) === 'all';
|
|
93
|
+
const row = all ? this.store.get('understandings', understandingId) : this.target({ sessionId, understandingId });
|
|
94
|
+
if (!row) fail('understanding_not_found', '理解不存在。');
|
|
95
|
+
const history = [], seen = new Set();
|
|
96
|
+
let current = row;
|
|
97
|
+
while (current && !seen.has(current.id) && (all || inScope(current, sessionId))) {
|
|
98
|
+
seen.add(current.id); history.push(current);
|
|
99
|
+
current = current.previousId ? this.store.get('understandings', current.previousId) : null;
|
|
100
|
+
}
|
|
101
|
+
const commands = this.store.listAll('understanding_commands').filter(command => (all || command.sessionId === sessionId) &&
|
|
102
|
+
(command.understandingId === row.id || command.result?.after?.id === row.id)).reverse();
|
|
103
|
+
const successors = this.store.listAll('understandings').filter(item => item.previousId === row.id && (all || inScope(item, sessionId)));
|
|
104
|
+
return { ...this.browseView(row, sessionId), historyTotal: history.length, history: history.slice(0, 20).map(item => this.browseView(item, sessionId)),
|
|
105
|
+
commandsTotal: commands.length, commands: commands.slice(0, 20), successorsTotal: successors.length,
|
|
106
|
+
successors: successors.slice(0, 20).map(item => ({ id: item.id, revision: item.revision, version: item.version, status: item.status })) };
|
|
107
|
+
}
|
|
108
|
+
request(input, correction = false) {
|
|
109
|
+
allowedKeys(input, ['sessionId', 'requestId', 'understandingId', 'expectedRevision', 'userIdentity',
|
|
110
|
+
...(correction ? ['text'] : ['action', 'sourceRef'])]);
|
|
111
|
+
this.session(input.sessionId);
|
|
112
|
+
for (const key of ['requestId', 'understandingId', 'userIdentity']) identity(input[key], key);
|
|
113
|
+
if (!positive(input.expectedRevision)) fail('invalid_understanding_request', '需要所见理解的明确版本。');
|
|
114
|
+
if (correction) {
|
|
115
|
+
if (typeof input.text !== 'string' || !input.text.trim() || input.text.length > 16000 || input.text.includes('\0'))
|
|
116
|
+
fail('invalid_correction_text', '请提交一段不超过 16000 字符的完整新表述;原字和换行会保留。');
|
|
117
|
+
} else if (!['stop-understanding', 'withdraw-source'].includes(input.action)) {
|
|
118
|
+
fail('invalid_understanding_action', '只接受停用理解或停止把原件当依据。');
|
|
119
|
+
}
|
|
120
|
+
return copy({ ...input, action: correction ? 'correct' : input.action });
|
|
121
|
+
}
|
|
122
|
+
prior(request) {
|
|
123
|
+
const prior = this.store.get('understanding_commands', commandId(request.sessionId, request.requestId));
|
|
124
|
+
if (prior && (prior.requestHash !== digest(request) || canonicalJson(prior.request) !== canonicalJson(request)))
|
|
125
|
+
fail('understanding_identity_conflict', '同一请求编号不能更换操作、原话、版本或用户身份。');
|
|
126
|
+
return prior;
|
|
127
|
+
}
|
|
128
|
+
savedCommand(request, fields = {}) {
|
|
129
|
+
return this.store.save('understanding_commands', { id: commandId(request.sessionId, request.requestId),
|
|
130
|
+
sessionId: request.sessionId, requestId: request.requestId, understandingId: request.understandingId,
|
|
131
|
+
action: request.action, request, requestHash: digest(request), actor: 'user',
|
|
132
|
+
provenance: { kind: 'trusted_owner_understanding_control', userIdentity: request.userIdentity },
|
|
133
|
+
admission: null, result: null, ...fields });
|
|
134
|
+
}
|
|
135
|
+
goalViews() {
|
|
136
|
+
return new Map(this.store.listAll('goals').map(goal => {
|
|
137
|
+
const view = this.goals.view(goal);
|
|
138
|
+
return [goal.id, { id: goal.id, revision: goal.revision, status: goal.status, userControl: goal.userControl ?? null,
|
|
139
|
+
definitionAvailable: Boolean(view), progressAvailable: Boolean(view?.goal.progress),
|
|
140
|
+
coverage: view?.coverage ?? null, relations: view?.goal.relations.map(row => ({ type: row.type,
|
|
141
|
+
targetId: row.targetId, state: row.state })) ?? null }];
|
|
142
|
+
}));
|
|
143
|
+
}
|
|
144
|
+
impacts(before) {
|
|
145
|
+
const after = this.goalViews();
|
|
146
|
+
return [...after].flatMap(([id, value]) => canonicalJson(before.get(id) ?? null) === canonicalJson(value)
|
|
147
|
+
? [] : [{ id, before: before.get(id) ?? null, after: value }]);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
decide(input) {
|
|
151
|
+
const request = this.request(input);
|
|
152
|
+
if (request.action === 'withdraw-source') {
|
|
153
|
+
allowedKeys(request.sourceRef, ['id', 'revision', 'expectedControlRevision']);
|
|
154
|
+
identity(request.sourceRef.id, 'Original source id');
|
|
155
|
+
if (!positive(request.sourceRef.revision) || !(request.sourceRef.expectedControlRevision === null || positive(request.sourceRef.expectedControlRevision)))
|
|
156
|
+
fail('invalid_source_control', '停止依据须绑定原件版本与当前控制版本,首次控制使用 null。');
|
|
157
|
+
} else if (request.sourceRef !== undefined) fail('invalid_source_control', '停用理解不能附带原件操作。');
|
|
158
|
+
return this.store.transaction(() => {
|
|
159
|
+
const prior = this.prior(request);
|
|
160
|
+
if (prior) return { command: prior, created: false };
|
|
161
|
+
const before = this.target(request), goalsBefore = this.goalViews();
|
|
162
|
+
const id = commandId(request.sessionId, request.requestId);
|
|
163
|
+
let after = before, control = null, invalidIds, reason;
|
|
164
|
+
if (request.action === 'stop-understanding') {
|
|
165
|
+
if (before.status !== 'active' || before.deleted) fail('understanding_unavailable', '所见理解已停用、替换或删除。');
|
|
166
|
+
after = this.store.save('understandings', { ...before, status: 'withdrawn',
|
|
167
|
+
changedByCommandId: id, withdrawalReason: 'understanding_stopped_by_user' }, { expectedRevision: before.revision });
|
|
168
|
+
invalidIds = [before.id]; reason = 'understanding_stopped_by_user';
|
|
169
|
+
} else {
|
|
170
|
+
if (!sourcesFor(this.store, before).some(source => source.id === request.sourceRef.id))
|
|
171
|
+
fail('source_scope_denied', '只能停止这条已显示理解的真实来源链中的原件。');
|
|
172
|
+
const source = this.store.get('events', request.sourceRef.id), existing = this.store.get('source_controls', request.sourceRef.id);
|
|
173
|
+
if (!source || source.revision !== request.sourceRef.revision || (existing?.revision ?? null) !== request.sourceRef.expectedControlRevision)
|
|
174
|
+
fail('revision_conflict', '原件或其控制状态已变化,请重新查看。');
|
|
175
|
+
if (!sourceAvailable(this.store, source)) fail('source_unavailable', '该原件已经停止作为依据。');
|
|
176
|
+
control = this.store.save('source_controls', { ...existing, id: source.id, useProhibited: true,
|
|
177
|
+
sourceRevision: source.revision, commandId: id, reason: 'source_withdrawn_by_user', actor: 'user',
|
|
178
|
+
provenance: { kind: 'trusted_owner_source_control', userIdentity: request.userIdentity } },
|
|
179
|
+
{ expectedRevision: existing?.revision ?? null });
|
|
180
|
+
invalidIds = [source.id]; reason = 'source_withdrawn_by_user';
|
|
181
|
+
}
|
|
182
|
+
const changes = invalidateDependents(this.store, { ids: invalidIds, pendingId: id, reason });
|
|
183
|
+
after = this.store.get('understandings', before.id);
|
|
184
|
+
const command = this.savedCommand(request, { result: { state: 'applied', before, after, control, changes,
|
|
185
|
+
goalImpacts: this.impacts(goalsBefore), meaning: 'future Arcfold eligibility; original native history and prior actions remain' } });
|
|
186
|
+
return { command, created: true };
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
bindCorrection(input) {
|
|
191
|
+
const request = this.request(input, true);
|
|
192
|
+
return this.store.transaction(() => {
|
|
193
|
+
const prior = this.prior(request);
|
|
194
|
+
if (prior) return { command: prior, created: false };
|
|
195
|
+
const old = this.target(request);
|
|
196
|
+
this.assertCorrectable(old);
|
|
197
|
+
return { command: this.savedCommand(request), created: true };
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
isCorrectable(row) {
|
|
201
|
+
// Source invalidation may mark a superseded historical row withdrawn. A
|
|
202
|
+
// real successor still makes it historical, regardless of either status.
|
|
203
|
+
return !row.deleted && ['active', 'withdrawn'].includes(row.status) && positive(row.version) &&
|
|
204
|
+
positive(row.version + 1) && this.store.list('understandings', { where: { previousId: row.id }, limit: 1 }).length === 0;
|
|
205
|
+
}
|
|
206
|
+
assertCorrectable(row) {
|
|
207
|
+
if (!this.isCorrectable(row))
|
|
208
|
+
fail('understanding_unavailable', '只能纠正保有版本身份的当前理解;旧版本仍可查看或撤回来源,请选择当前版本纠正。');
|
|
209
|
+
}
|
|
210
|
+
assertCommand(command) {
|
|
211
|
+
if (!command || command.action !== 'correct' || digest(command.request) !== command.requestHash ||
|
|
212
|
+
command.id !== commandId(command.request.sessionId, command.request.requestId) ||
|
|
213
|
+
command.sessionId !== command.request.sessionId || command.requestId !== command.request.requestId ||
|
|
214
|
+
command.understandingId !== command.request.understandingId)
|
|
215
|
+
fail('understanding_command_changed', '纠正绑定缺失或已被改写。');
|
|
216
|
+
}
|
|
217
|
+
recordCorrectionAdmission({ commandId: id, state }) {
|
|
218
|
+
if (!['accepted', 'failed', 'unknown'].includes(state)) fail('invalid_correction_admission', '需要实际原生提交入口的结果。');
|
|
219
|
+
return this.store.transaction(() => {
|
|
220
|
+
const command = this.store.get('understanding_commands', id); this.assertCommand(command);
|
|
221
|
+
if (command.admission) {
|
|
222
|
+
if (command.admission.state !== state) fail('correction_admission_conflict', '提交结果已保存,不可改换或重发。');
|
|
223
|
+
return command;
|
|
224
|
+
}
|
|
225
|
+
return this.store.save('understanding_commands', { ...command, admission: { state,
|
|
226
|
+
evidence: 'native-prompt-entry-result; not-proof-of-durable-owner-input' } }, { expectedRevision: command.revision });
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// This internal call receives the exact claims returned by ingestClaimedInputs,
|
|
231
|
+
// plus its durable prefix. No public RPC accepts claims, events or source rows.
|
|
232
|
+
applyClaimedCorrections({ sessionId, claims, events }) {
|
|
233
|
+
this.session(sessionId);
|
|
234
|
+
if (this.store.get('host_bindings', sessionId)?.role !== 'main')
|
|
235
|
+
fail('understanding_session_denied', '应用纠正需要实际主交流的执行关联。');
|
|
236
|
+
if (!Array.isArray(claims) || !Array.isArray(events)) fail('invalid_correction_claims', '纠正需要真实持久收件箱原件。');
|
|
237
|
+
const claimed = claims.map(claim => {
|
|
238
|
+
const source = this.store.get('events', claim.sourceId), ref = claim.inboxRef;
|
|
239
|
+
const inbox = events.filter(event => event.type === 'agent/inbox/spliced' && event.seq === ref?.seq);
|
|
240
|
+
const saved = ref && this.store.get('dsh_events', eventId(sessionId, ref.seq));
|
|
241
|
+
if (!source || source.sessionId !== sessionId || source.role !== 'user' || source.message?.role !== 'user' ||
|
|
242
|
+
source.message.id !== claim.messageId || source.message.source?.kind !== 'user' ||
|
|
243
|
+
source.message.source.principal !== undefined || digest(source.message) !== claim.messageHash ||
|
|
244
|
+
ref?.sessionId !== sessionId || ref.type !== 'agent/inbox/spliced' || !Number.isSafeInteger(ref.index) || ref.index < 0 ||
|
|
245
|
+
inbox.length !== 1 || digest(inbox[0]) !== ref.sha256 || saved?.sha256 !== ref.sha256 ||
|
|
246
|
+
canonicalJson(inbox[0].data.inserted?.[ref.index] ?? null) !== canonicalJson(source.message))
|
|
247
|
+
fail('invalid_correction_claims', '纠正不能脱离持久的原生 owner 用户收件箱原件。');
|
|
248
|
+
return { source, claim };
|
|
249
|
+
});
|
|
250
|
+
if (new Set(claimed.map(row => row.source.id)).size !== claimed.length)
|
|
251
|
+
fail('invalid_correction_claims', '本次用户原件身份不能重复。');
|
|
252
|
+
// Current native request identities give direct record keys; ordinary steps
|
|
253
|
+
// do not scan or replay every past correction in this conversation.
|
|
254
|
+
const commands = [...new Set(claimed.map(row => row.source.message.source.rpcId).filter(id => typeof id === 'string'))]
|
|
255
|
+
.map(requestId => this.store.get('understanding_commands', commandId(sessionId, requestId)))
|
|
256
|
+
.filter(command => command?.action === 'correct');
|
|
257
|
+
const results = [];
|
|
258
|
+
for (const command of commands) {
|
|
259
|
+
if (command.result) continue;
|
|
260
|
+
this.assertCommand(command);
|
|
261
|
+
try {
|
|
262
|
+
results.push(this.store.transaction(() => {
|
|
263
|
+
const current = this.store.get('understanding_commands', command.id); this.assertCommand(current);
|
|
264
|
+
if (current.result) return current;
|
|
265
|
+
const originals = this.store.listAll('events', { where: { sessionId } }).filter(source =>
|
|
266
|
+
source.message?.source?.kind === 'user' && source.message.source.principal === undefined &&
|
|
267
|
+
source.message.source.rpcId === current.requestId && source.role === 'user');
|
|
268
|
+
const matches = claimed.filter(row => row.source.message.source.rpcId === current.requestId);
|
|
269
|
+
if (originals.length !== 1 || matches.length !== 1 || originals[0].id !== matches[0].source.id ||
|
|
270
|
+
canonicalJson(matches[0].source.message.content) !== canonicalJson([{ type: 'text', text: current.request.text }]))
|
|
271
|
+
fail('correction_source_mismatch', '原生原话的编号或全文不再唯一匹配所确认的纠正。');
|
|
272
|
+
const { source, claim } = matches[0];
|
|
273
|
+
if (!sourceAvailable(this.store, source)) fail('source_unavailable', '纠正原件已经停止作为依据。');
|
|
274
|
+
const before = this.target(current.request); this.assertCorrectable(before);
|
|
275
|
+
const goalsBefore = this.goalViews();
|
|
276
|
+
const retired = this.store.save('understandings', { ...before, status: 'superseded',
|
|
277
|
+
changedByCommandId: current.id, changeSourceIds: [source.id] }, { expectedRevision: before.revision });
|
|
278
|
+
const changes = invalidateDependents(this.store, { ids: [before.id], pendingId: current.id,
|
|
279
|
+
reason: 'understanding_corrected_by_user' });
|
|
280
|
+
const after = this.store.save('understandings', projectUnderstanding({
|
|
281
|
+
id: 'understanding-' + digest(current.id).slice(0, 32), content: current.request.text,
|
|
282
|
+
claimType: 'user_statement', scope: copy(before.scope ?? {}), status: 'active',
|
|
283
|
+
sourceIds: [source.id], dependencies: [{ kind: 'events', id: source.id, revision: source.revision }],
|
|
284
|
+
version: before.version + 1, previousId: before.id, createdByCommandId: current.id,
|
|
285
|
+
authority: 'literal-user-statement; revisable; not truth or action permission' },
|
|
286
|
+
[{ ...source, content: sourceText(source) }]));
|
|
287
|
+
return this.store.save('understanding_commands', { ...current, result: { state: 'applied',
|
|
288
|
+
before, retired, after, sourceId: source.id, sourceRevision: source.revision, claim: copy(claim),
|
|
289
|
+
changes, goalImpacts: this.impacts(goalsBefore),
|
|
290
|
+
evidence: 'durable-native-owner-inbox; final request guard still requires actual native user/message' } },
|
|
291
|
+
{ expectedRevision: current.revision });
|
|
292
|
+
}));
|
|
293
|
+
} catch (error) {
|
|
294
|
+
if (!(error instanceof DomainError)) throw error;
|
|
295
|
+
// Ingestion is already committed. Keep the real original even when the
|
|
296
|
+
// old knowledge version or confirmed text cannot be safely replaced.
|
|
297
|
+
results.push(this.store.transaction(() => {
|
|
298
|
+
const current = this.store.get('understanding_commands', command.id); this.assertCommand(current);
|
|
299
|
+
if (current.result) return current;
|
|
300
|
+
return this.store.save('understanding_commands', { ...current, result: { state: 'failed',
|
|
301
|
+
error: { code: error.code, message: error.message },
|
|
302
|
+
claimedSourceIds: claimed.filter(row => row.source.message.source.rpcId === current.requestId).map(row => row.source.id),
|
|
303
|
+
meaning: 'native input preserved; no replacement or automatic resend' } }, { expectedRevision: current.revision });
|
|
304
|
+
}));
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return results;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
5
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
6
|
+
import { BudgetLedger, webBudgetIdentity } from './budget.mjs';
|
|
7
|
+
import { Store, canonicalJson, assertWebBudgetFileFormat } from './store.mjs';
|
|
8
|
+
import { assertEvidenceOwnership, canonicalEvidenceDirectory, ownEvidenceDirectory } from './evidence-ownership.mjs';
|
|
9
|
+
|
|
10
|
+
export const WEB_BUDGET_RECOVERY_VERSION = 'web-only-recovery-v1';
|
|
11
|
+
const suffixes = ['', '-journal', '-wal', '-shm'];
|
|
12
|
+
const sha = bytes => createHash('sha256').update(bytes).digest('hex');
|
|
13
|
+
function present(path) {
|
|
14
|
+
try { fs.lstatSync(path); return true; }
|
|
15
|
+
catch (error) { if (error.code === 'ENOENT') return false; throw error; }
|
|
16
|
+
}
|
|
17
|
+
const fail = message => Object.assign(new Error(message), { code: 'web_budget_recovery_refused' });
|
|
18
|
+
|
|
19
|
+
export function webBudgetPath(artifactDirectory, ledgerPath) {
|
|
20
|
+
const directory = canonicalEvidenceDirectory(artifactDirectory), filename = join(directory, 'deepseek-web.sqlite');
|
|
21
|
+
if (ledgerPath !== undefined && (!isAbsolute(ledgerPath) || resolve(ledgerPath) !== filename))
|
|
22
|
+
throw fail('The web-only ledger must use the fixed canonical evidence-directory path');
|
|
23
|
+
return filename;
|
|
24
|
+
}
|
|
25
|
+
function regular(path) {
|
|
26
|
+
const info = fs.lstatSync(path);
|
|
27
|
+
if (!info.isFile() || info.nlink !== 1 || info.uid !== process.getuid() || (info.mode & 0o077))
|
|
28
|
+
throw fail('Web-only ledger files must be private, single-link regular files');
|
|
29
|
+
return info;
|
|
30
|
+
}
|
|
31
|
+
export function preflightWebBudget({ artifactDirectory, ledgerPath }) {
|
|
32
|
+
const filename = webBudgetPath(artifactDirectory, ledgerPath);
|
|
33
|
+
regular(filename); assertWebBudgetFileFormat(filename);
|
|
34
|
+
for (const suffix of suffixes.slice(1)) {
|
|
35
|
+
if (!present(filename + suffix)) continue;
|
|
36
|
+
regular(filename + suffix);
|
|
37
|
+
if (suffix !== '-journal') throw fail('The web-only rollback format cannot contain WAL/SHM files');
|
|
38
|
+
}
|
|
39
|
+
return filename;
|
|
40
|
+
}
|
|
41
|
+
function inventory(filename) {
|
|
42
|
+
return suffixes.map(suffix => {
|
|
43
|
+
const path = filename + suffix;
|
|
44
|
+
if (!present(path)) return { suffix, missing: true };
|
|
45
|
+
const info = regular(path), bytes = fs.readFileSync(path);
|
|
46
|
+
const after = regular(path);
|
|
47
|
+
if (info.ino !== after.ino || info.dev !== after.dev || info.size !== after.size || info.mtimeMs !== after.mtimeMs)
|
|
48
|
+
throw fail('The original ledger changed while being copied');
|
|
49
|
+
return { suffix, size: bytes.length, sha256: sha(bytes), dev: info.dev, ino: info.ino,
|
|
50
|
+
mode: info.mode, uid: info.uid, mtimeMs: info.mtimeMs };
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
function syncDirectory(path) {
|
|
54
|
+
const fd = fs.openSync(path, 'r'); try { fs.fsyncSync(fd); } finally { fs.closeSync(fd); }
|
|
55
|
+
}
|
|
56
|
+
function save(path, value) {
|
|
57
|
+
const fd = fs.openSync(path, 'wx', 0o600);
|
|
58
|
+
try { fs.writeFileSync(fd, JSON.stringify(value, null, 2) + '\n'); fs.fsyncSync(fd); }
|
|
59
|
+
finally { fs.closeSync(fd); }
|
|
60
|
+
syncDirectory(join(path, '..'));
|
|
61
|
+
}
|
|
62
|
+
function copySet(filename, directory, manifest) {
|
|
63
|
+
fs.mkdirSync(directory, { mode: 0o700 });
|
|
64
|
+
const target = join(directory, 'deepseek-web.sqlite');
|
|
65
|
+
for (const item of manifest) {
|
|
66
|
+
if (item.missing) continue;
|
|
67
|
+
fs.copyFileSync(filename + item.suffix, target + item.suffix, fs.constants.COPYFILE_EXCL);
|
|
68
|
+
fs.chmodSync(target + item.suffix, 0o600);
|
|
69
|
+
assert.equal(sha(fs.readFileSync(target + item.suffix)), item.sha256, 'Copied ledger bytes differ');
|
|
70
|
+
const fd = fs.openSync(target + item.suffix, 'r');
|
|
71
|
+
try { fs.fsyncSync(fd); } finally { fs.closeSync(fd); }
|
|
72
|
+
}
|
|
73
|
+
syncDirectory(directory); return target;
|
|
74
|
+
}
|
|
75
|
+
function records(db) {
|
|
76
|
+
return {
|
|
77
|
+
applicationId: db.prepare('PRAGMA application_id').get().application_id,
|
|
78
|
+
schema: db.prepare('SELECT type,name,tbl_name,sql FROM sqlite_schema ORDER BY type,name').all().map(row => ({ ...row })),
|
|
79
|
+
records: db.prepare('SELECT * FROM records ORDER BY kind,id').all().map(row => ({ ...row })),
|
|
80
|
+
meta: db.prepare('SELECT * FROM store_meta ORDER BY key').all().map(row => ({ ...row })),
|
|
81
|
+
requests: db.prepare('SELECT * FROM budget_requests ORDER BY id').all().map(row => ({ ...row })),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function recoveredConnection(filename, identity) {
|
|
85
|
+
let db;
|
|
86
|
+
try {
|
|
87
|
+
db = new DatabaseSync(filename);
|
|
88
|
+
db.exec('PRAGMA busy_timeout=30000; PRAGMA synchronous=FULL');
|
|
89
|
+
const integrity = db.prepare('PRAGMA integrity_check').all();
|
|
90
|
+
assert.deepEqual(integrity.map(row => row.integrity_check), ['ok'], 'The recovered ledger failed SQLite integrity validation');
|
|
91
|
+
const store = Store.fromWebBudgetConnection(db, { filename, webBudgetIdentity: identity });
|
|
92
|
+
return { store, snapshot: records(db) };
|
|
93
|
+
} catch (error) { db?.close(); throw error; }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Both entry points below require the real held lease; no config bypass flag. */
|
|
97
|
+
function inspectCopy({ artifactDirectory, ledgerPath, webAuthorization, owner }) {
|
|
98
|
+
assertEvidenceOwnership(owner, artifactDirectory);
|
|
99
|
+
const filename = preflightWebBudget({ artifactDirectory, ledgerPath }), identity = webBudgetIdentity(webAuthorization);
|
|
100
|
+
const evidenceRoot = canonicalEvidenceDirectory(artifactDirectory);
|
|
101
|
+
const recoveryRoot = join(evidenceRoot, 'web-budget-recovery');
|
|
102
|
+
// lstat also detects dangling symlinks. No archive mkdir or original SQLite
|
|
103
|
+
// recovery is allowed through a redirected fixed child of the held owner.
|
|
104
|
+
if (present(recoveryRoot) && !fs.lstatSync(recoveryRoot).isDirectory())
|
|
105
|
+
throw fail('The recovery archive root must be a real directory within its owned evidence root');
|
|
106
|
+
if (canonicalEvidenceDirectory(recoveryRoot) !== recoveryRoot)
|
|
107
|
+
throw fail('The recovery archive root cannot be an alias');
|
|
108
|
+
const recoveryDirectory = join(recoveryRoot, randomUUID());
|
|
109
|
+
fs.mkdirSync(recoveryDirectory, { recursive: true, mode: 0o700 });
|
|
110
|
+
syncDirectory(join(recoveryDirectory, '..'));
|
|
111
|
+
syncDirectory(canonicalEvidenceDirectory(artifactDirectory));
|
|
112
|
+
let originalRecoveryStarted = false;
|
|
113
|
+
const failure = error => {
|
|
114
|
+
save(join(recoveryDirectory, 'failure.json'), { code: error.code ?? 'recovery_failed',
|
|
115
|
+
message: error.message, originalRecoveryStarted });
|
|
116
|
+
};
|
|
117
|
+
try {
|
|
118
|
+
const manifest = inventory(filename);
|
|
119
|
+
save(join(recoveryDirectory, 'original.json'), { filename, manifest });
|
|
120
|
+
const archive = copySet(filename, join(recoveryDirectory, 'original'), manifest);
|
|
121
|
+
assert.deepEqual(inventory(filename), manifest, 'Original ledger changed during archival');
|
|
122
|
+
const working = copySet(archive, join(recoveryDirectory, 'working'), manifest);
|
|
123
|
+
const copy = recoveredConnection(working, identity);
|
|
124
|
+
let snapshot;
|
|
125
|
+
try { snapshot = copy.snapshot; save(join(recoveryDirectory, 'recovered-records.json'), snapshot); }
|
|
126
|
+
finally { copy.store.close(); }
|
|
127
|
+
const assertOriginal = () => {
|
|
128
|
+
assertEvidenceOwnership(owner, artifactDirectory);
|
|
129
|
+
assert.deepEqual(inventory(filename), manifest, 'Original ledger changed after copy validation');
|
|
130
|
+
};
|
|
131
|
+
assertOriginal();
|
|
132
|
+
return { filename, identity, recoveryDirectory, snapshot, assertOriginal, failure,
|
|
133
|
+
beginOriginal() { originalRecoveryStarted = true;
|
|
134
|
+
save(join(recoveryDirectory, 'original-recovery-started.json'), { filename, at: new Date().toISOString() }); } };
|
|
135
|
+
} catch (error) { failure(error); throw error; }
|
|
136
|
+
}
|
|
137
|
+
export function validateOwnedWebBudgetCopy(options) {
|
|
138
|
+
const inspected = inspectCopy(options);
|
|
139
|
+
save(join(inspected.recoveryDirectory, 'validated-copy.json'), { mode: WEB_BUDGET_RECOVERY_VERSION,
|
|
140
|
+
originalModified: false, recordsHash: sha(canonicalJson(inspected.snapshot)) });
|
|
141
|
+
return { recoveryDirectory: inspected.recoveryDirectory, recordsHash: sha(canonicalJson(inspected.snapshot)) };
|
|
142
|
+
}
|
|
143
|
+
export function openOwnedWebBudget(options) {
|
|
144
|
+
const inspected = inspectCopy(options); let original;
|
|
145
|
+
try {
|
|
146
|
+
inspected.assertOriginal(); inspected.beginOriginal();
|
|
147
|
+
original = recoveredConnection(inspected.filename, inspected.identity);
|
|
148
|
+
assert.deepEqual(original.snapshot, inspected.snapshot, 'Original recovery differs from the validated copy');
|
|
149
|
+
assertEvidenceOwnership(options.owner, options.artifactDirectory);
|
|
150
|
+
const ledger = BudgetLedger.fromWebBudgetStore(original.store, options.webAuthorization);
|
|
151
|
+
save(join(inspected.recoveryDirectory, 'completed.json'), { mode: WEB_BUDGET_RECOVERY_VERSION,
|
|
152
|
+
recordsHash: sha(canonicalJson(original.snapshot)), authorizationId: inspected.identity.id });
|
|
153
|
+
return { ledger, recoveryDirectory: inspected.recoveryDirectory };
|
|
154
|
+
} catch (error) {
|
|
155
|
+
original?.store.close(); inspected.failure(error); throw error;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Called from a verified installed core, never by importing native peers in the unpacked CLI. */
|
|
160
|
+
export async function initializeOwnedWebBudget({ artifactDirectory, ledgerPath, webAuthorization }) {
|
|
161
|
+
const filename = webBudgetPath(artifactDirectory, ledgerPath), identity = webBudgetIdentity(webAuthorization);
|
|
162
|
+
const owner = await ownEvidenceDirectory(artifactDirectory); let ledger;
|
|
163
|
+
try {
|
|
164
|
+
const created = !present(filename);
|
|
165
|
+
if (created) {
|
|
166
|
+
for (const suffix of suffixes.slice(1))
|
|
167
|
+
if (present(filename + suffix)) throw fail('A missing database cannot reclaim existing recovery files');
|
|
168
|
+
ledger = new BudgetLedger(filename, { mode: 'web-only', scope: 'web-unlimited', webAuthorization, create: true });
|
|
169
|
+
} else ({ ledger } = openOwnedWebBudget({ artifactDirectory, ledgerPath: filename, webAuthorization, owner }));
|
|
170
|
+
return { ledgerPath: filename, created, authorizationId: identity.id };
|
|
171
|
+
} finally { try { ledger?.close(); } finally { await owner.close(); } }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export async function validateWebBudgetVersion(routes, artifactDirectory) {
|
|
175
|
+
const selected = Object.values(routes).filter(route => route.mode === 'web-only');
|
|
176
|
+
if (!selected.length) return [];
|
|
177
|
+
const owner = await ownEvidenceDirectory(artifactDirectory);
|
|
178
|
+
try { return selected.map(route => validateOwnedWebBudgetCopy({ artifactDirectory,
|
|
179
|
+
ledgerPath: route.ledgerPath, webAuthorization: route.webAuthorization, owner })); }
|
|
180
|
+
finally { await owner.close(); }
|
|
181
|
+
}
|