@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.
Files changed (127) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +8 -2
  3. package/THIRD_PARTY_NOTICES.md +11 -0
  4. package/cordis.patch.yml +138 -0
  5. package/package.json +70 -4
  6. package/patches/deepseek-0.2.2-rc.1-transport.json +65 -0
  7. package/patches/deepseek-0.2.2-rc.1-transport.patch +61 -0
  8. package/presets/arcfold.patch.yml +110 -0
  9. package/runtime/README.md +19 -0
  10. package/runtime/bootstrap.json +15 -0
  11. package/runtime/desktop-python.tar.gz +0 -0
  12. package/runtime/host/package.json +11 -0
  13. package/runtime/host/pnpm-workspace.yaml +9 -0
  14. package/runtime/initial-policy.json +94 -0
  15. package/runtime/profile/pnpm-workspace.yaml +7 -0
  16. package/scripts/exact-package-patch.mjs +106 -0
  17. package/scripts/patch-deepseek-transport.mjs +14 -0
  18. package/src/agent.mjs +56 -0
  19. package/src/browse-scope.mjs +14 -0
  20. package/src/budget.mjs +369 -0
  21. package/src/business.mjs +334 -0
  22. package/src/context-material.mjs +119 -0
  23. package/src/deepseek-budget-plugin.mjs +58 -0
  24. package/src/deepseek-provider.mjs +57 -0
  25. package/src/deepseek-receipt.mjs +97 -0
  26. package/src/deepseek-transport.mjs +97 -0
  27. package/src/desktop/adapter.mjs +16 -0
  28. package/src/desktop/bootstrap.mjs +84 -0
  29. package/src/desktop/configuration.mjs +117 -0
  30. package/src/desktop/provider.mjs +46 -0
  31. package/src/desktop/python-runtime.mjs +105 -0
  32. package/src/desktop/setup-service.mjs +81 -0
  33. package/src/desktop/vendor/LICENSE.deepseek +21 -0
  34. package/src/desktop/vendor/NOTICE.md +5 -0
  35. package/src/desktop/vendor/deepseek-0.2.2-rc.1.mjs +2919 -0
  36. package/src/evidence-ownership.mjs +51 -0
  37. package/src/evidence.mjs +35 -0
  38. package/src/goal-context.mjs +52 -0
  39. package/src/goal-execution-tool.mjs +74 -0
  40. package/src/goal-execution.mjs +333 -0
  41. package/src/goal-tool.mjs +56 -0
  42. package/src/goal-work-control.mjs +63 -0
  43. package/src/goal-work.mjs +257 -0
  44. package/src/goals.mjs +578 -0
  45. package/src/importer.mjs +298 -0
  46. package/src/index.mjs +53 -0
  47. package/src/learning-contracts.mjs +220 -0
  48. package/src/learning-defaults.mjs +98 -0
  49. package/src/learning-goals.mjs +165 -0
  50. package/src/learning-plugin.mjs +28 -0
  51. package/src/learning-runner.mjs +275 -0
  52. package/src/learning.mjs +534 -0
  53. package/src/legacy-export.mjs +332 -0
  54. package/src/legacy-import-source.mjs +356 -0
  55. package/src/metered-provider-host.mjs +101 -0
  56. package/src/metered-transport.mjs +435 -0
  57. package/src/method-context.mjs +57 -0
  58. package/src/method-lifecycle.mjs +34 -0
  59. package/src/method-tool.mjs +61 -0
  60. package/src/methods.mjs +425 -0
  61. package/src/model-selection.mjs +41 -0
  62. package/src/native-context-call.mjs +88 -0
  63. package/src/native-continuation.mjs +217 -0
  64. package/src/native-request-authority.mjs +90 -0
  65. package/src/openai-models.mjs +4 -0
  66. package/src/openai-receipt.mjs +98 -0
  67. package/src/openai-transport.mjs +167 -0
  68. package/src/openai-usage.mjs +194 -0
  69. package/src/opportunities.mjs +295 -0
  70. package/src/preparation-artifacts.mjs +213 -0
  71. package/src/preparation-plugin.mjs +290 -0
  72. package/src/runtime-command.mjs +264 -0
  73. package/src/runtime-config.mjs +417 -0
  74. package/src/runtime-guard.mjs +284 -0
  75. package/src/runtime-host.mjs +212 -0
  76. package/src/runtime-init.mjs +278 -0
  77. package/src/runtime-installation.mjs +518 -0
  78. package/src/runtime-lifecycle.mjs +227 -0
  79. package/src/runtime-model-settings.mjs +111 -0
  80. package/src/runtime-readiness.mjs +285 -0
  81. package/src/runtime-workspace.mjs +18 -0
  82. package/src/session-bridge.mjs +365 -0
  83. package/src/store.mjs +394 -0
  84. package/src/tree/LICENSE +21 -0
  85. package/src/tree/attribution.mjs +120 -0
  86. package/src/tree/clusters.mjs +61 -0
  87. package/src/tree/consolidation.mjs +103 -0
  88. package/src/tree/context.mjs +211 -0
  89. package/src/tree/contracts.mjs +142 -0
  90. package/src/tree/experience.mjs +140 -0
  91. package/src/tree/experiment-budget.mjs +84 -0
  92. package/src/tree/hooks.mjs +254 -0
  93. package/src/tree/host-context.mjs +107 -0
  94. package/src/tree/host-process.mjs +288 -0
  95. package/src/tree/host.mjs +37 -0
  96. package/src/tree/improvement.mjs +106 -0
  97. package/src/tree/items.mjs +294 -0
  98. package/src/tree/jobs.mjs +184 -0
  99. package/src/tree/learning.mjs +202 -0
  100. package/src/tree/metrics.mjs +203 -0
  101. package/src/tree/model-owner.mjs +254 -0
  102. package/src/tree/observations.mjs +73 -0
  103. package/src/tree/prompts.mjs +500 -0
  104. package/src/tree/reports.mjs +67 -0
  105. package/src/tree/repository.mjs +236 -0
  106. package/src/tree/runtime.mjs +745 -0
  107. package/src/tree/script-writer.mjs +149 -0
  108. package/src/tree/scripts.mjs +208 -0
  109. package/src/tree/signals.mjs +118 -0
  110. package/src/tree/snapshots.mjs +379 -0
  111. package/src/tree/token-counter.mjs +75 -0
  112. package/src/tree/token-counter.py +37 -0
  113. package/src/tree/tokenizer-resources/LICENSE +21 -0
  114. package/src/tree/tokenizer-resources/README.md +31 -0
  115. package/src/tree/tokenizer-resources/deepseek_recipe-0.2.0-cp310-abi3-macosx_11_0_arm64.whl +0 -0
  116. package/src/tree/tokenizer-resources/manifest.json +61 -0
  117. package/src/tree/tokenizer-resources/native.pyi +1042 -0
  118. package/src/tree/tokenizer-resources/tokenizer-NOTICE.md +26 -0
  119. package/src/tree/tokenizer-resources/tokenizer-v4-NOTICE.md +27 -0
  120. package/src/tree/tokenizer-resources/tokenizer-v4.json +267359 -0
  121. package/src/tree/tokenizer-resources/tokenizer.json +267359 -0
  122. package/src/tree/tools.mjs +99 -0
  123. package/src/tree/tree.mjs +202 -0
  124. package/src/tree/ui.mjs +285 -0
  125. package/src/tree/verification.mjs +143 -0
  126. package/src/understandings.mjs +309 -0
  127. package/src/web-budget-recovery.mjs +181 -0
@@ -0,0 +1,534 @@
1
+ import { worksForDelivery, assertWork, relationState } from './goal-work.mjs';
2
+ import { assertNativeContinuationStored } from './native-continuation.mjs';
3
+ // Business-only migration of 3.0 learning.mjs material/commitSection/save*.
4
+ // DSH and the host own execution, retries, dispatch identities and durability.
5
+ import { canonicalJson, identity } from './store.mjs';
6
+ import { messageText } from './business.mjs';
7
+ import { sourceText, evidenceProvenance } from './evidence.mjs';
8
+ import { pauseMethod } from './method-lifecycle.mjs';
9
+ import { usageForVersion } from './methods.mjs';
10
+ import { GoalService, goalRepairAllowed } from './goals.mjs';
11
+ import { ContractError, digest, clone, LEARN_PROMPT, LEARN_SCHEMA, LEARN_SECTIONS,
12
+ parseLearn, assertVisibleSources, requiresUserSource, projectUnderstanding } from './learning-contracts.mjs';
13
+ import { applyGoalChanges, invalidateDependents } from './learning-goals.mjs';
14
+
15
+ export { LEARN_PROMPT, LEARN_SCHEMA } from './learning-contracts.mjs';
16
+ const groupName = kind => 'existing' + kind[0].toUpperCase() + kind.slice(1);
17
+ const unavailable = row => !row || row.deleted || row.withdrawn || row.useProhibited ||
18
+ ['withdrawn', 'superseded', 'rejected', 'dismissed'].includes(row.status);
19
+ const atSequence = (row, cutoff) => Number.isSafeInteger(row?.createdSequence) &&
20
+ Number.isSafeInteger(row.updatedSequence) && row.createdSequence <= cutoff && row.updatedSequence <= cutoff;
21
+ const fail = (code, message) => { throw new ContractError(code, message); };
22
+ const recordRef = (kind, record) => ({ kind, id: record.id, revision: record.revision, hash: digest(record) });
23
+ const errorView = error => ({ code: error.code || 'learning_commit_failed', message: error.message,
24
+ ...(error.details ? { details: error.details } : {}) });
25
+
26
+ function sourceCurrent(store, source) {
27
+ const control = source && store.get('source_controls', source.id);
28
+ return !unavailable(source) && !unavailable(control || {}) && control?.status !== 'paused';
29
+ }
30
+
31
+ function sourceBefore(store, source, boundary) {
32
+ if (!sourceCurrent(store, source) || !atSequence(source, boundary.sequence)) return false;
33
+ const ref = source.provenance?.sourceRef;
34
+ if (ref?.sessionId === boundary.sessionId && ref.seq > boundary.throughSeq) return false;
35
+ return typeof source.occurredAt === 'string' && Number.isFinite(Date.parse(source.occurredAt)) &&
36
+ Date.parse(source.occurredAt) <= Date.parse(boundary.occurredAt);
37
+ }
38
+
39
+ // Unlike a method dependency, a paused goal root can still be discussed.
40
+ function evidenceBundle(store, row, kind, boundary, path = new Set()) {
41
+ const key = `${kind}:${row?.id}`;
42
+ const rejected = kind === 'opportunities'
43
+ ? !row || row.deleted || row.withdrawn || row.useProhibited : unavailable(row);
44
+ if (rejected || row.status === 'paused' && !['goals', 'opportunities'].includes(kind) ||
45
+ !atSequence(row, boundary.sequence) || path.has(key) ||
46
+ !Array.isArray(row.sourceIds) || !row.sourceIds.length) return null;
47
+ const next = new Set([...path, key]), sources = new Map(), records = new Map([[key, recordRef(kind, row)]]);
48
+ for (const id of row.sourceIds) {
49
+ const source = store.get('events', id);
50
+ if (!sourceBefore(store, source, boundary)) return null;
51
+ sources.set(id, source);
52
+ }
53
+ for (const ref of row.dependencies || []) {
54
+ const depKind = ref.kind || ['events', 'understandings', 'methods'].find(k => store.get(k, ref.id));
55
+ if (!['events', 'understandings', 'methods'].includes(depKind)) return null;
56
+ const dependency = store.get(depKind, ref.id);
57
+ if (!dependency || dependency.revision !== ref.revision) return null;
58
+ if (depKind === 'events') {
59
+ if (!sourceBefore(store, dependency, boundary)) return null;
60
+ sources.set(dependency.id, dependency);
61
+ } else {
62
+ const nested = evidenceBundle(store, dependency, depKind, boundary, next);
63
+ if (!nested) return null;
64
+ for (const source of nested.sources) sources.set(source.id, source);
65
+ for (const version of nested.records) records.set(`${version.kind}:${version.id}`, version);
66
+ }
67
+ }
68
+ return { sources: [...sources.values()], records: [...records.values()] };
69
+ }
70
+
71
+ function sourceView(source) {
72
+ return { id: source.id, revision: source.revision, role: source.role,
73
+ content: source.message ? messageText(source.message) : sourceText(source),
74
+ occurredAt: source.occurredAt, conversationId: source.conversationId ?? null,
75
+ provenance: evidenceProvenance(source.provenance),
76
+ ...(source.message?.content.some(block => !['text', 'reasoning'].includes(block.type))
77
+ ? { nonTextBlocks: source.message.content.filter(block => !['text', 'reasoning'].includes(block.type)) } : {}),
78
+ ...(source.role === 'assistant' ? { contentAuthor: 'assistant', userEndorsement: 'not_established_by_this_record' } : {}) };
79
+ }
80
+
81
+ function toolTrace(source) {
82
+ return source.role === 'tool' ? { id: source.id, sourceId: source.id,
83
+ toolCallId: source.message?.toolCallId ?? null, isError: source.message?.isError === true } : null;
84
+ }
85
+
86
+ export class LearningService {
87
+ constructor({ business, maxCharacters = 180000 }) {
88
+ if (!business?.storage || !Number.isSafeInteger(maxCharacters) || maxCharacters < 1000 || maxCharacters > 180000)
89
+ throw new ContractError('invalid_learning_config', '需要业务存储和有效材料容量');
90
+ this.business = business; this.store = business.storage; this.maxCharacters = maxCharacters;
91
+ }
92
+
93
+ prepare(pendingId) {
94
+ identity(pendingId, 'Pending learning id');
95
+ return this.store.transaction(() => {
96
+ const existing = this.store.get('learning_inputs', pendingId);
97
+ if (existing) {
98
+ this.assertInput(existing);
99
+ this.assertVersions(existing);
100
+ return existing;
101
+ }
102
+ const pending = this.store.get('learning_pending', pendingId);
103
+ if (!pending || pending.status !== 'pending') fail('invalid_learning_pending', '没有尚待处理的学习业务记录');
104
+ const delivery = this.store.get('deliveries', pending.deliveryId);
105
+ if (!delivery || delivery.kind !== 'formal_reply' || delivery.sessionId !== pending.sessionId ||
106
+ delivery.throughSeq !== pending.throughSeq) fail('invalid_learning_delivery', '学习须对应真实正式交付');
107
+ const boundary = { sequence: delivery.createdSequence, sessionId: delivery.sessionId,
108
+ throughSeq: delivery.throughSeq, occurredAt: delivery.occurredAt };
109
+ const sources = new Map(), versions = new Map();
110
+ if (delivery.nativeContinuation) assertNativeContinuationStored(this.store, delivery.nativeContinuation);
111
+ for (const id of [...new Set([...delivery.sourceEventIds,
112
+ ...((delivery.nativeContinuation?.activeDelegation ?? delivery.nativeContinuation?.initialDelegation)?.sourceIds || [])])]) {
113
+ const source = this.store.get('events', id);
114
+ if (!sourceBefore(this.store, source, boundary)) fail('source_changed', '正式交付原件已失效或越过截止');
115
+ sources.set(id, source);
116
+ }
117
+ if (!sources.has(delivery.eventId)) fail('invalid_learning_delivery', '正式回复原件缺失');
118
+ const material = { version: 'arcfold-dsh-learning-v1', pendingId, sessionId: delivery.sessionId,
119
+ deliveryId: delivery.id, deliveredAnswerId: delivery.eventId,
120
+ inputEventIds: delivery.inputEventIds, inputEventId: delivery.inputEventIds.at(-1) || null,
121
+ boundary, authority: 'answer-already-delivered; source-grounded-business-interpretations; no-native-execution-or-user-endorsement-authority',
122
+ ...(delivery.nativeContinuation ? { nativeContinuation: delivery.nativeContinuation } : {}),
123
+ sources: [], existingUnderstandings: [], existingMethods: [], existingOpportunities: [], existingGoals: [],
124
+ coverage: {}, toolReceipts: [], backgroundAtTurnEnd: delivery.backgroundAtTurnEnd };
125
+ for (const kind of LEARN_SECTIONS) {
126
+ const coverage = material.coverage[kind] = { available: 0, included: 0, omitted: 0 };
127
+ for (const row of this.store.listAll(kind)) {
128
+ if (row.scope?.conversationId && row.scope.conversationId !== delivery.sessionId ||
129
+ kind === 'opportunities' && row.conversationId !== delivery.sessionId) continue;
130
+ coverage.available++;
131
+ const validStatus = kind === 'understandings' ? row.status === 'active'
132
+ : kind === 'methods' ? ['trial', 'active'].includes(row.status) : true;
133
+ const bundle = validStatus && evidenceBundle(this.store, row, kind, boundary);
134
+ if (!bundle) { coverage.omitted++; continue; }
135
+ for (const source of bundle.sources) sources.set(source.id, source);
136
+ for (const ref of bundle.records) versions.set(`${ref.kind}:${ref.id}`, ref);
137
+ // Progress and each relationship have their own sources and versions.
138
+ const view = clone(row);
139
+ // Usage is a projection of adoption receipts, never a method revision.
140
+ if (kind === 'methods') view.trialUses = usageForVersion(this.store, row, { cutoffSequence: boundary.sequence }).trialUses;
141
+ if (kind === 'goals') {
142
+ const validPart = part => {
143
+ if (!part) return false;
144
+ if (!part.sourceIds?.length) return part.content === '';
145
+ const partBundle = evidenceBundle(this.store, { ...part, id: `part:${row.id}`,
146
+ createdSequence: row.createdSequence, updatedSequence: row.updatedSequence }, 'goal_part', boundary);
147
+ if (!partBundle) return false;
148
+ for (const source of partBundle.sources) sources.set(source.id, source);
149
+ for (const ref of partBundle.records.filter(ref => ref.kind !== 'goal_part')) versions.set(`${ref.kind}:${ref.id}`, ref);
150
+ return true;
151
+ };
152
+ if (!validPart(view.progress)) view.progress = null;
153
+ view.relations = (view.relations || []).filter(validPart).map(relation => {
154
+ const target = this.store.get('goals', relation.targetId);
155
+ return { ...relation, state: unavailable(target) ? 'unavailable'
156
+ : target.definitionVersion !== relation.targetDefinitionVersion ||
157
+ (relation.sourceDefinitionVersion ?? row.definitionVersion) !== row.definitionVersion ? 'needs_review' : 'current' };
158
+ });
159
+ }
160
+ material[groupName(kind)].push(view); coverage.included++;
161
+ }
162
+ }
163
+ const goals = new GoalService({ business: this.business });
164
+ const inputMessageIds = delivery.inputEventIds.map(id => this.store.get('events', id)?.message?.id).filter(Boolean);
165
+ material.goalDiscussions = goals.discussions({ sessionId: delivery.sessionId, inputMessageIds,
166
+ cutoffSequence: boundary.sequence }).map(binding => {
167
+ const source = this.store.get('events', binding.sourceId);
168
+ if (!material.inputEventIds.includes(source?.id) || !sourceBefore(this.store, source, boundary))
169
+ fail('source_changed', '目标讨论没有本次真实用户原件');
170
+ // Admission status is a later transport observation, not mutable user intent.
171
+ versions.set(`goal_discussions:${binding.id}`, { kind: 'goal_discussions', id: binding.id,
172
+ requestHash: binding.requestHash });
173
+ if (binding.mode === 'repair') {
174
+ const goal = this.store.get('goals', binding.goalId);
175
+ if (!goalRepairAllowed(goal) || !atSequence(goal, boundary.sequence))
176
+ fail('source_changed', '目标修复资格已改变');
177
+ material.existingGoals = material.existingGoals.filter(row => row.id !== goal.id);
178
+ material.existingGoals.push({ id: goal.id, revision: goal.revision,
179
+ definitionVersion: goal.definitionVersion, status: goal.status, repairOnly: true,
180
+ reason: goal.sourceInvalidation.reason || 'source_dependency', repairSourceId: source.id });
181
+ versions.set(`goals:${goal.id}`, recordRef('goals', goal));
182
+ }
183
+ return { id: binding.id, goalId: binding.goalId, goalRevision: binding.goalRevision,
184
+ definitionVersion: binding.definitionVersion, mode: binding.mode,
185
+ sourceId: source.id, messageId: binding.messageId };
186
+ });
187
+ material.goalLinks = this.store.listAll('goal_links', { where: { sessionId: delivery.sessionId,
188
+ startSeq: delivery.startSeq } }).flatMap(link => {
189
+ if (link.turn !== delivery.turn || link.createdSequence > boundary.sequence) return [];
190
+ goals.assertCore(link);
191
+ const outcome = this.store.get('goal_link_outcomes', link.id);
192
+ // The immutable association predates delivery; only this exact derived
193
+ // end reference may have been saved after delivery.createdSequence.
194
+ if (!outcome || outcome.deliveryId !== delivery.id || outcome.coreHash !== link.coreHash ||
195
+ outcome.sessionId !== delivery.sessionId || outcome.turn !== delivery.turn ||
196
+ outcome.startSeq !== delivery.startSeq || outcome.endEventId !== delivery.endEventId) return [];
197
+ versions.set(`goal_links:${link.id}`, recordRef('goal_links', link));
198
+ versions.set(`goal_link_outcomes:${link.id}`, recordRef('goal_link_outcomes', outcome));
199
+ const entry = { linkId: link.id, goalId: link.goalId, goalRevision: link.goalRevision,
200
+ definitionVersion: link.definitionVersion, deliveryId: delivery.id, outcome: outcome.outcome,
201
+ contributionEffect: 'not_assessed', claimType: 'assistant_inference' };
202
+ const records = [link.bundle.goal, ...link.bundle.relatedGoals];
203
+ const current = records.every(shown => {
204
+ const row = this.store.get('goals', shown.id);
205
+ return row?.revision === shown.revision && atSequence(row, boundary.sequence) &&
206
+ (shown.repairOnly ? goalRepairAllowed(row) : Boolean(evidenceBundle(this.store, row, 'goals', boundary)));
207
+ }) && link.bundle.sources.every(shown => {
208
+ const row = this.store.get('events', shown.id);
209
+ return row?.revision === shown.revision && sourceBefore(this.store, row, boundary);
210
+ });
211
+ if (!current) return [{ ...entry, availability: 'source_or_definition_changed', selection: null }];
212
+ for (const shown of link.bundle.sources) sources.set(shown.id, this.store.get('events', shown.id));
213
+ for (const shown of records) versions.set(`goals:${shown.id}`, recordRef('goals', this.store.get('goals', shown.id)));
214
+ return [{ ...entry, availability: 'current', selection: link.selection,
215
+ inputEvidence: this.store.listAll('goal_inputs', { where: { linkId: link.id } })
216
+ .filter(row => row.createdSequence <= boundary.sequence)
217
+ .map(({ id, step, requestIdentity, inputHash, evidence }) => ({ id, step, requestIdentity, inputHash, evidence })) }];
218
+ });
219
+ material.goalWorks = worksForDelivery(this.store, delivery).map(work => {
220
+ assertWork(this.store, work);
221
+ if (!work.state) fail('goal_work_changed', '工作缺少截止前的原生状态');
222
+ for (const id of work.sourceIds) {
223
+ const source = this.store.get('events', id);
224
+ if (!sourceBefore(this.store, source, boundary)) fail('source_changed', '工作委托来源不在此次交付截止内');
225
+ sources.set(id, source);
226
+ }
227
+ versions.set('goal_works:' + work.id, recordRef('goal_works', this.store.get('goal_works', work.id)));
228
+ return { id: work.id, sessionId: work.sessionId, nativeGoalId: work.nativeGoalId,
229
+ ...work.state, id: work.id, relationId: work.relationId,
230
+ relation: relationState(this.store, work, work.relationId && this.store.get('goal_work_links', work.relationId), work.state),
231
+ sourceIds: work.sourceIds,
232
+ originKind: work.originKind, authority: 'historical-work-facts; no-new-permission' };
233
+ });
234
+ material.sources = [...sources.values()].sort((a, b) => a.createdSequence - b.createdSequence).map(sourceView);
235
+ material.toolReceipts = [...sources.values()].map(toolTrace).filter(Boolean);
236
+ material.existingUnderstandings = material.existingUnderstandings.map(row => projectUnderstanding(row, material.sources));
237
+ for (const source of sources.values()) {
238
+ versions.set(`events:${source.id}`, recordRef('events', source));
239
+ const control = this.store.get('source_controls', source.id);
240
+ versions.set(`source_controls:${source.id}`, { kind: 'source_controls', id: source.id,
241
+ revision: control?.revision ?? null, hash: control ? digest(control) : null });
242
+ }
243
+ const inputText = JSON.stringify(material);
244
+ if (LEARN_PROMPT.length + inputText.length > this.maxCharacters ||
245
+ Buffer.byteLength(LEARN_PROMPT + inputText) > 512000)
246
+ fail('learning_capacity', '完整学习材料超过容量,未截断原件或冻结部分输入');
247
+ return this.store.save('learning_inputs', { id: pendingId, pendingId, deliveryId: delivery.id,
248
+ material, versions: [...versions.values()], prompt: LEARN_PROMPT, schema: LEARN_SCHEMA,
249
+ inputText, inputHash: digest(inputText), materialHash: digest(material), promptHash: digest(LEARN_PROMPT) });
250
+ });
251
+ }
252
+
253
+ assertInput(input) {
254
+ if (input.inputText !== JSON.stringify(input.material) || input.inputHash !== digest(input.inputText) ||
255
+ input.materialHash !== digest(input.material) || input.promptHash !== digest(input.prompt))
256
+ fail('learning_input_changed', '冻结学习输入身份不一致');
257
+ }
258
+
259
+ ownChanges(pendingId) {
260
+ const changes = new Map();
261
+ for (const section of this.store.listAll('learning_sections', { where: { pendingId } }))
262
+ if (section.status === 'completed') for (const change of section.changes) changes.set(`${change.kind}:${change.id}`, change);
263
+ return changes;
264
+ }
265
+
266
+ assertVersions(input) {
267
+ if (input.material.nativeContinuation) assertNativeContinuationStored(this.store, input.material.nativeContinuation);
268
+ const own = this.ownChanges(input.pendingId);
269
+ for (const ref of input.versions) {
270
+ if (ref.kind === 'goal_discussions') {
271
+ const current = this.store.get(ref.kind, ref.id);
272
+ if (!current || current.requestHash !== ref.requestHash || digest(current.request) !== ref.requestHash)
273
+ fail('source_changed', '真实用户目标选择已变化');
274
+ continue;
275
+ }
276
+ const current = this.store.get(ref.kind, ref.id), accepted = own.get(`${ref.kind}:${ref.id}`) || ref;
277
+ if ((current?.revision ?? null) !== accepted.revision || (current ? digest(current) : null) !== accepted.hash)
278
+ fail('source_changed', `学习材料版本已变化:${ref.kind}/${ref.id}`);
279
+ if (ref.kind === 'events' && !sourceCurrent(this.store, current)) fail('source_changed', '学习原件已撤回');
280
+ }
281
+ }
282
+
283
+ assertTarget(input, kind, id) {
284
+ const shown = input.material[groupName(kind)]?.find(row => row.id === id);
285
+ const current = this.store.get(kind, id);
286
+ if (kind === 'goals' && shown?.repairOnly === true) {
287
+ const discussion = input.material.goalDiscussions.find(row => row.goalId === id && row.mode === 'repair');
288
+ if (!current || current.revision !== shown.revision || !goalRepairAllowed(current) ||
289
+ !atSequence(current, input.material.boundary.sequence) || !discussion ||
290
+ !input.material.inputEventIds.includes(discussion.sourceId) ||
291
+ !sourceBefore(this.store, this.store.get('events', discussion.sourceId), input.material.boundary))
292
+ fail('learning_conflict', '目标修复缺少当前明确选回或新原话');
293
+ return current;
294
+ }
295
+ if (!shown || !current || current.revision !== shown.revision ||
296
+ !evidenceBundle(this.store, current, kind, input.material.boundary))
297
+ fail('learning_conflict', '目标或依赖未显示、已改变或已失效');
298
+ return current;
299
+ }
300
+
301
+ sourceReferences(input, ids, requireUser = false) {
302
+ const refs = assertVisibleSources(ids, input.material.sources, { requireUser });
303
+ for (const ref of refs) {
304
+ const current = this.store.get('events', ref.id);
305
+ if (current?.revision !== ref.revision || !sourceBefore(this.store, current, input.material.boundary))
306
+ fail('source_changed', '提议引用的原件已变化或越过截止');
307
+ }
308
+ return refs;
309
+ }
310
+
311
+ understandingReferences(input, ids) {
312
+ if (new Set(ids).size !== ids.length) fail('invalid_source', '理解依赖不可重复');
313
+ return ids.map(id => {
314
+ const current = this.assertTarget(input, 'understandings', id);
315
+ if (current.status !== 'active') fail('learning_conflict', '理解依赖已失效');
316
+ return { kind: 'understandings', id, revision: current.revision };
317
+ });
318
+ }
319
+
320
+ /** Only consumes a flushed host-bound auxiliary result; never starts a session. */
321
+ commit(pendingId, { sessionId, events }) {
322
+ const input = this.store.get('learning_inputs', pendingId);
323
+ if (!input) fail('learning_not_prepared', '尚未冻结学习输入');
324
+ this.assertInput(input);
325
+ const run = this.store.get('learning_runs', pendingId);
326
+ if (!run || run.sessionId !== sessionId || !['submitted', 'settled'].includes(run.state) ||
327
+ run.inputHash !== input.inputHash || run.message?.id !== run.messageId)
328
+ fail('learning_run_mismatch', '辅助结果没有匹配的可信宿主登记');
329
+ const output = auxiliaryOutput(input, run, events);
330
+ const outputId = pendingId;
331
+ this.store.transaction(() => {
332
+ const prior = this.store.get('learning_outputs', outputId);
333
+ if (prior && prior.outputHash !== output.outputHash) fail('learning_output_conflict', '同次学习不能改换输出或辅助事件');
334
+ if (!prior) this.store.save('learning_outputs', { id: outputId, pendingId, ...output });
335
+ });
336
+ let parsed;
337
+ try {
338
+ if (output.error) throw new ContractError(output.error.code, output.error.message);
339
+ parsed = parseLearn(output.rawText);
340
+ } catch (error) {
341
+ for (const section of LEARN_SECTIONS) this.recordFailure(pendingId, section, error);
342
+ return this.finish(pendingId);
343
+ }
344
+ for (const section of LEARN_SECTIONS) {
345
+ if (this.store.get('learning_sections', `${pendingId}:${section}`)) continue;
346
+ try {
347
+ if (parsed.issues[section].length) throw new ContractError('invalid_output', '学习分区格式不符合合同', parsed.issues[section]);
348
+ this.commitSection(input, section, parsed.value[section]);
349
+ } catch (error) { this.recordFailure(pendingId, section, error); }
350
+ }
351
+ return this.finish(pendingId);
352
+ }
353
+
354
+ commitSection(input, section, rows) {
355
+ return this.store.transaction(() => {
356
+ this.assertVersions(input);
357
+ const before = new Map(LEARN_SECTIONS.flatMap(kind => this.store.listAll(kind)
358
+ .map(row => [`${kind}:${row.id}`, recordRef(kind, row)])));
359
+ let ids;
360
+ if (section === 'goals') ids = applyGoalChanges(this.store, {
361
+ pendingId: input.pendingId, material: input.material, rows,
362
+ assertTarget: (kind, id) => this.assertTarget(input, kind, id),
363
+ assertSources: (ids, required) => this.sourceReferences(input, ids, required),
364
+ understandingRefs: ids => this.understandingReferences(input, ids),
365
+ });
366
+ else {
367
+ const targets = new Set();
368
+ ids = rows.map((row, index) => {
369
+ if (row.targetId && targets.has(row.targetId)) fail('invalid_output', '同一分区不能重复修改一个目标');
370
+ if (row.targetId) targets.add(row.targetId);
371
+ const refs = section === 'opportunities' && row.operation === 'reuse' ? []
372
+ : this.sourceReferences(input, row.sourceIds, requiresUserSource(section, row));
373
+ return section === 'understandings' ? this.saveUnderstanding(input, row, index, refs)
374
+ : section === 'methods' ? this.saveMethod(input, row, index, refs)
375
+ : this.saveOpportunity(input, row, index, refs);
376
+ });
377
+ }
378
+ const changes = LEARN_SECTIONS.flatMap(kind => this.store.listAll(kind).flatMap(row => {
379
+ const previous = before.get(`${kind}:${row.id}`);
380
+ return previous?.hash !== digest(row) ? [recordRef(kind, row)] : [];
381
+ }));
382
+ return this.store.save('learning_sections', { id: `${input.pendingId}:${section}`, pendingId: input.pendingId,
383
+ section, status: 'completed', ids, changes });
384
+ });
385
+ }
386
+
387
+ recordFailure(pendingId, section, error) {
388
+ const id = `${pendingId}:${section}`;
389
+ this.store.transaction(() => {
390
+ if (!this.store.get('learning_sections', id)) this.store.save('learning_sections', {
391
+ id, pendingId, section, status: 'failed', ids: [], changes: [], error: errorView(error) });
392
+ });
393
+ }
394
+
395
+ finish(pendingId) {
396
+ return this.store.transaction(() => {
397
+ const sections = this.store.listAll('learning_sections', { where: { pendingId } });
398
+ if (sections.length !== 4) fail('incomplete_learning', '学习分区尚未全部结算');
399
+ const completed = sections.filter(section => section.status === 'completed').length;
400
+ const status = completed === 4 ? 'completed' : completed ? 'partial' : 'failed';
401
+ const pending = this.store.get('learning_pending', pendingId);
402
+ if (!pending) fail('invalid_learning_pending', '学习待办已缺失');
403
+ const result = { status, sections: Object.fromEntries(sections.map(section => [section.section, section])) };
404
+ if (pending.status !== status || digest(pending.result || null) !== digest(result))
405
+ this.store.save('learning_pending', { ...pending, status, result }, { expectedRevision: pending.revision });
406
+ return result;
407
+ });
408
+ }
409
+
410
+ saveUnderstanding(input, row, index, dependencies) {
411
+ const old = row.operation === 'create' ? null : this.assertTarget(input, 'understandings', row.targetId);
412
+ if (row.operation === 'create' && row.targetId || !row.content.trim()) fail('invalid_output', '理解内容或目标编号不合法');
413
+ if (old && old.status !== 'active') fail('learning_conflict', '只可修订有效理解');
414
+ if (old) {
415
+ this.store.save('understandings', { ...old, status: row.operation === 'withdraw' ? 'withdrawn' : 'superseded',
416
+ changeSourceIds: row.sourceIds, changedByRunId: input.pendingId }, { expectedRevision: old.revision });
417
+ invalidateDependents(this.store, { ids: [old.id], pendingId: input.pendingId,
418
+ reason: `understanding_${row.operation}_by_learning` });
419
+ }
420
+ if (row.operation === 'withdraw') return old.id;
421
+ const saved = this.store.save('understandings', projectUnderstanding({
422
+ id: 'understanding-' + digest([input.pendingId, index]).slice(0, 32), content: row.content, claimType: row.claimType,
423
+ scope: { ...(row.scope === 'conversation' ? { conversationId: input.material.sessionId } : {}), topics: row.topics },
424
+ status: 'active', sourceIds: row.sourceIds, dependencies, version: (old?.version || 0) + 1,
425
+ previousId: old?.id || null, learnedFromRunId: input.pendingId, authority: 'revisable-understanding; not-permission',
426
+ }, input.material.sources));
427
+ return saved.id;
428
+ }
429
+
430
+ saveMethod(input, row, index, dependencies) {
431
+ const old = row.operation === 'create' ? null : this.assertTarget(input, 'methods', row.targetId);
432
+ if (row.operation === 'create' && row.targetId ||
433
+ ![row.title, row.guidance, row.appliesTo, row.doesNotApplyTo].every(value => value.trim()))
434
+ fail('invalid_output', '方法须有完整内容、适用与不适用条件和合法目标编号');
435
+ dependencies.push(...this.understandingReferences(input, row.understandingIds));
436
+ if (old) {
437
+ pauseMethod(this.store, old, { reason: row.operation === 'pause' ? row.guidance : 'superseded',
438
+ changes: { changeSourceIds: row.sourceIds }, pendingId: input.pendingId });
439
+ invalidateDependents(this.store, { ids: [old.id], pendingId: input.pendingId, reason: `method_${row.operation}_by_learning` });
440
+ }
441
+ if (row.operation === 'pause') return old.id;
442
+ const signature = digest({ title: row.title, guidance: row.guidance, appliesTo: row.appliesTo, doesNotApplyTo: row.doesNotApplyTo });
443
+ // Include paused/exhausted historical methods; a new id cannot buy a new trial.
444
+ const duplicate = this.store.listAll('methods').find(method => method.signature === signature ||
445
+ digest({ title: method.title ?? '', guidance: method.guidance ?? '', appliesTo: method.appliesTo ?? '', doesNotApplyTo: method.doesNotApplyTo ?? '' }) === signature);
446
+ if (duplicate) return duplicate.id;
447
+ return this.store.save('methods', { id: 'method-' + digest([input.pendingId, index]).slice(0, 32),
448
+ title: row.title, guidance: row.guidance, appliesTo: row.appliesTo, doesNotApplyTo: row.doesNotApplyTo,
449
+ origin: 'inferred', proposedOrigin: row.origin, sourceIds: row.sourceIds, dependencies,
450
+ status: 'trial', maxTrialUses: 3, validated: false, signature,
451
+ version: (old?.version || 0) + 1, previousId: old?.id || null, learnedFromRunId: input.pendingId,
452
+ evidence: [{ kind: 'learning-source', runId: input.pendingId, sourceIds: row.sourceIds }],
453
+ authority: 'work-guidance-only; no-new-tool-or-permission' }).id;
454
+ }
455
+
456
+ saveOpportunity(input, row, index, dependencies) {
457
+ if (row.operation === 'reuse') {
458
+ const old = this.assertTarget(input, 'opportunities', row.targetId);
459
+ if (row.sourceIds.length || [row.description, row.rationale, row.suggestedPreparation].some(value => value !== ''))
460
+ fail('invalid_output', '复用机会只能关联旧编号,不能更新内容或恢复执行');
461
+ return old.id;
462
+ }
463
+ if (row.targetId || ![row.description, row.rationale, row.suggestedPreparation].every(value => value.trim()))
464
+ fail('invalid_output', '机会须有完整内容且新建不可指向旧编号');
465
+ const signature = digest({ conversationId: input.material.sessionId, description: row.description,
466
+ rationale: row.rationale, suggestedPreparation: row.suggestedPreparation, sourceIds: [...row.sourceIds].sort() });
467
+ const duplicate = this.store.listAll('opportunities').find(item => item.signature === signature);
468
+ if (duplicate) return duplicate.id;
469
+ return this.store.save('opportunities', { id: 'opportunity-' + digest([input.pendingId, index]).slice(0, 32),
470
+ conversationId: input.material.sessionId, description: row.description, rationale: row.rationale,
471
+ suggestedPreparation: row.suggestedPreparation, sourceIds: row.sourceIds, dependencies,
472
+ status: 'pending', preparationStatus: 'not_authorized', preparationRunId: null, artifactIds: [], signature,
473
+ learnedFromRunId: input.pendingId, authority: 'unendorsed-option; no-execution-authority' }).id;
474
+ }
475
+ }
476
+
477
+ function auxiliaryOutput(input, run, rawEvents) {
478
+ if (!Array.isArray(rawEvents) || !rawEvents.length) fail('invalid_auxiliary_history', '辅助会话原事件缺失');
479
+ const events = clone(rawEvents);
480
+ for (const [seq, event] of events.entries()) if (event.seq !== seq || typeof event.type !== 'string' || !event.data)
481
+ fail('invalid_auxiliary_history', '辅助会话必须提供从零开始的完整原事件');
482
+ const starts = events.filter(event => event.type === 'turn/start');
483
+ const ends = events.filter(event => event.type === 'turn/end');
484
+ const originals = events.filter(event => event.type === 'user/message' && event.data.id === run.messageId);
485
+ const expected = run.message;
486
+ if (expected.source?.kind !== 'arcfold/learning' || expected.source.pendingId !== input.pendingId ||
487
+ expected.source.deliveryId !== input.deliveryId || messageText(expected) !== input.inputText ||
488
+ originals.length !== 1 || canonicalJson(originals[0].data) !== canonicalJson(expected))
489
+ fail('learning_input_mismatch', '实际辅助消息与冻结材料或业务归属不一致');
490
+ const final = events.findLast(event => event.type === 'assistant/message');
491
+ const rawText = final ? messageText(final.data.message) : '';
492
+ const error = (code, message) => ({ code, message });
493
+ let problem = null;
494
+ if (starts.length !== 1 || ends.length !== 1 || starts[0].seq >= ends[0].seq ||
495
+ starts[0].data.turn !== ends[0].data.turn || events.some(event => event.type === 'session/end-seed' && event.data.inherited))
496
+ problem = error('invalid_auxiliary_turn', '学习只接受一轮非继承的完整辅助输出');
497
+ else if (ends[0].data.reason?.kind !== 'completed') problem = error('auxiliary_incomplete', '辅助会话未正常完成');
498
+ else if (!final || final.seq <= starts[0].seq || final.seq >= ends[0].seq ||
499
+ final.data.turn !== starts[0].data.turn || final.data.interrupted || final.data.message?.source?.kind !== 'model' ||
500
+ !rawText.trim()) problem = error('auxiliary_incomplete', '学习没有完整模型正文');
501
+ else if (events.some(event => ['tool/call', 'tool/result', 'job/registered', 'subagent/catalog'].includes(event.type)) ||
502
+ events.some(event => event.type === 'assistant/message' && event.data.message.content.some(block => block.type === 'tool-call')))
503
+ problem = error('auxiliary_tools_forbidden', '学习业务不接受工具或后台执行分支');
504
+ else if (events.some(event => event.type === 'user/message' && event.data.id !== run.messageId &&
505
+ event.data.source?.kind !== 'runtime-context'))
506
+ problem = error('auxiliary_extra_input', '冻结输入之后辅助会话又收到其他消息');
507
+ else if (originals[0].seq <= starts[0].seq || originals[0].seq >= final.seq)
508
+ problem = error('auxiliary_input_order', '冻结输入不在本轮模型输出之前');
509
+ if (!problem) {
510
+ let openStep = null;
511
+ for (const event of events.filter(event => event.seq > starts[0].seq && event.seq < ends[0].seq)) {
512
+ if (event.type === 'step/start') {
513
+ if (openStep !== null || event.data.turn !== starts[0].data.turn || !Number.isSafeInteger(event.data.step)) {
514
+ problem = error('auxiliary_step_boundary', '辅助步骤边界不完整'); break;
515
+ }
516
+ openStep = event.data.step;
517
+ } else if (event.type === 'step/end') {
518
+ if (openStep === null || event.data.step !== openStep || event.data.turn !== starts[0].data.turn) {
519
+ problem = error('auxiliary_step_boundary', '辅助步骤边界不完整'); break;
520
+ }
521
+ openStep = null;
522
+ } else if (['assistant/message', 'assistant/attempt'].includes(event.type) &&
523
+ (openStep === null || event.data.step !== openStep || event.data.turn !== starts[0].data.turn)) {
524
+ problem = error('auxiliary_step_boundary', '辅助模型输出不属于已进入的步骤'); break;
525
+ }
526
+ }
527
+ if (openStep !== null) problem = error('auxiliary_step_boundary', '辅助步骤尚未关闭');
528
+ }
529
+ return { sessionId: run.sessionId, messageId: run.messageId,
530
+ throughSeq: events.at(-1).seq, eventCount: events.length,
531
+ sourceRef: { sessionId: run.sessionId, seq: final?.seq ?? null, type: final?.type ?? null },
532
+ rawText, eventHash: final ? digest(final) : null,
533
+ outputHash: digest({ sessionId: run.sessionId, events }), error: problem };
534
+ }