@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,334 @@
1
+ import { assertNativeContinuationIntegrity } from './native-continuation.mjs';
2
+ import { createHash } from 'node:crypto';
3
+ import { Store, DomainError, canonicalJson, identity, jsonClone } from './store.mjs';
4
+
5
+ const MESSAGE_TYPES = new Set(['user/message', 'assistant/message', 'tool/result']);
6
+ const SURFACE_TYPES = new Set([...MESSAGE_TYPES, 'system/message', 'developer/message']);
7
+ const hash = value => createHash('sha256').update(canonicalJson(value)).digest('hex');
8
+ export const eventId = (sessionId, seq) => `dsh-event:${hash([sessionId, seq])}`;
9
+ const turnId = (sessionId, turn) => `dsh-turn:${hash([sessionId, turn])}`;
10
+
11
+ /** Read visible text without persisting a second copy of message blocks. */
12
+ export function messageText(message) {
13
+ return (message?.content || []).filter(block => block.type === 'text').map(block => block.text).join('');
14
+ }
15
+
16
+ function eventBatch(sessionId, events) {
17
+ identity(sessionId, 'Session id');
18
+ if (!Array.isArray(events)) throw new DomainError('invalid_events', 'Events must be an array.', 400);
19
+ const copy = jsonClone(events);
20
+ for (let i = 0; i < copy.length; i++) {
21
+ const event = copy[i];
22
+ if (!event || typeof event !== 'object' || Array.isArray(event) ||
23
+ !Number.isSafeInteger(event.seq) || event.seq < 0 ||
24
+ !Number.isSafeInteger(event.time) || !Number.isFinite(new Date(event.time).getTime()) ||
25
+ !event.data || typeof event.data !== 'object' || Array.isArray(event.data))
26
+ throw new DomainError('invalid_event', 'Expected a lossless DSH event with seq, time and data.', 400);
27
+ identity(event.type, 'Event type');
28
+ if (i && event.seq !== copy[i - 1].seq + 1)
29
+ throw new DomainError('event_sequence_gap', 'Event batches must retain contiguous DSH order.');
30
+ }
31
+ return copy;
32
+ }
33
+
34
+ function validateMessage(message, role) {
35
+ if (!message || typeof message !== 'object' || !Array.isArray(message.content) ||
36
+ !message.source || typeof message.source !== 'object')
37
+ throw new DomainError('invalid_message', 'A DSH source message needs its content and producer.', 400);
38
+ identity(message.id, 'Message id'); identity(message.source.kind, 'Message producer');
39
+ if (message.role !== role) throw new DomainError('invalid_message_role', 'Event type and message role disagree.', 400);
40
+ for (const block of message.content) {
41
+ if (!block || typeof block !== 'object' || typeof block.type !== 'string' ||
42
+ (['text', 'reasoning'].includes(block.type) && typeof block.text !== 'string'))
43
+ throw new DomainError('invalid_message', 'Message blocks must preserve their declared type.', 400);
44
+ }
45
+ }
46
+
47
+ function sourceProjection(sessionId, event, sha256) {
48
+ if (!MESSAGE_TYPES.has(event.type)) return null;
49
+ const message = event.type === 'user/message' ? event.data : event.data.message;
50
+ const role = event.type === 'user/message' ? 'user' : event.type === 'assistant/message' ? 'assistant' : 'tool';
51
+ validateMessage(message, role);
52
+ // A user-role runtime snapshot/relay is never a human decision.
53
+ const author = message.source.kind === (role === 'assistant' ? 'model' : role) ? role : 'program';
54
+ const sourceRef = { sessionId, seq: event.seq, type: event.type, sha256 };
55
+ return { id: eventId(sessionId, event.seq), conversationId: sessionId, sessionId,
56
+ role: author, author, occurredAt: new Date(event.time).toISOString(),
57
+ provenance: { kind: 'dsh_session', source: message.source, sourceRef },
58
+ message, surfaceOp: event.surfaceOp ?? null,
59
+ sourceEventSeqs: event.sourceEventSeqs ?? [],
60
+ ...(event.type !== 'user/message' ? { turn: event.data.turn, step: event.data.step } : {}),
61
+ ...(event.data.interrupted === true ? { interrupted: true } : {}) };
62
+ }
63
+
64
+ function sourceIndex(sources) {
65
+ const index = new Map();
66
+ for (const source of sources) {
67
+ // Authentic command sources are business originals, not chat messages.
68
+ if (!source.message?.id) continue;
69
+ const id = source.message.id;
70
+ if (!index.has(id)) index.set(id, []);
71
+ index.get(id).push(source);
72
+ }
73
+ return index;
74
+ }
75
+
76
+ function sameMessage(sources, message) {
77
+ if (sources.some(source => canonicalJson(source.message) !== canonicalJson(message)))
78
+ throw new DomainError('source_identity_conflict', 'The same native message identity has different original content.');
79
+ }
80
+
81
+ // The caller owns the official claim boundary. Match its complete message to
82
+ // real durable insertion facts; this does not implement another inbox/driver.
83
+ function claimedOrigins(batch, messages) {
84
+ return messages.map(message => {
85
+ const origins = batch.flatMap(event => event.type === 'agent/inbox/spliced' && Array.isArray(event.data.inserted)
86
+ ? event.data.inserted.flatMap((input, index) => input?.id === message.id ? [{ message: input, event, index }] : []) : []);
87
+ if (!origins.length)
88
+ throw new DomainError('invalid_claimed_input', 'Claimed input has no durable native inbox original.');
89
+ // Official queue editing preserves message.id and replaces its content.
90
+ // Only the latest insertion can match the host's current claimed payload;
91
+ // sameMessage still protects any original already saved as a source.
92
+ const origin = origins.at(-1);
93
+ if (canonicalJson(origin.message) !== canonicalJson(message))
94
+ throw new DomainError('source_identity_conflict', 'Claimed input differs from its latest durable inbox original.');
95
+ return origin;
96
+ });
97
+ }
98
+
99
+ const metadata = new Set(['revision', 'createdAt', 'updatedAt', 'createdSequence', 'updatedSequence']);
100
+ function original(record) {
101
+ return Object.fromEntries(Object.entries(record).filter(([key]) => !metadata.has(key)));
102
+ }
103
+
104
+ /** Only business records live here. DSH remains the execution/event-log owner. */
105
+ export class BusinessStore {
106
+ constructor({ filename }) { this.storage = new Store(filename); }
107
+ get(...args) { return this.storage.get(...args); }
108
+ list(...args) { return this.storage.list(...args); }
109
+ listAll(...args) { return this.storage.listAll(...args); }
110
+ transaction(fn) { return this.storage.transaction(fn); }
111
+ close() { this.storage.close(); }
112
+
113
+ immutable(kind, record) {
114
+ const existing = this.get(kind, record.id);
115
+ if (existing) {
116
+ if (canonicalJson(original(existing)) !== canonicalJson(record))
117
+ throw new DomainError('source_identity_conflict', 'The same DSH identity has different original content.', 409,
118
+ { kind, id: record.id });
119
+ return existing;
120
+ }
121
+ return this.storage.save(kind, record);
122
+ }
123
+
124
+ /** Caller supplies the official claimed batch and a prefix covered by flush. */
125
+ ingestClaimedInputs({ sessionId, events, messages }) {
126
+ const batch = eventBatch(sessionId, events), inputs = jsonClone(messages);
127
+ if (!Array.isArray(inputs)) throw new DomainError('invalid_claimed_input', 'Claimed messages must be an array.', 400);
128
+ if (!inputs.length) return [];
129
+ if (!batch.length || batch[0].seq !== 0)
130
+ throw new DomainError('incomplete_history', 'Claimed input requires the complete durable native prefix.');
131
+ const ids = new Set();
132
+ for (const message of inputs) {
133
+ if (message?.role !== 'user' || message.source?.kind !== 'user' || message.source.principal !== undefined)
134
+ throw new DomainError('invalid_claimed_input', 'Only original owner user input can be admitted here.');
135
+ validateMessage(message, 'user');
136
+ if (ids.has(message.id)) throw new DomainError('invalid_claimed_input', 'Claimed message identities must be unique.');
137
+ ids.add(message.id);
138
+ }
139
+ const origins = claimedOrigins(batch, inputs);
140
+ return this.transaction(() => {
141
+ this.ingestSession({ sessionId, events: batch });
142
+ const index = sourceIndex(this.listAll('events', { where: { sessionId } }));
143
+ return inputs.map((message, i) => {
144
+ const origin = origins[i];
145
+ const inboxRef = { sessionId, seq: origin.event.seq, type: origin.event.type,
146
+ sha256: hash(origin.event), index: origin.index };
147
+ const previous = index.get(message.id) || [];
148
+ sameMessage(previous, message);
149
+ let source = previous[0];
150
+ if (!source) {
151
+ source = this.immutable('events', {
152
+ id: `dsh-inbox:${hash([sessionId, origin.event.seq, origin.index])}`,
153
+ conversationId: sessionId, sessionId, role: 'user', author: 'user',
154
+ occurredAt: new Date(origin.event.time).toISOString(), message,
155
+ provenance: { kind: 'dsh_session', source: message.source, sourceRef: inboxRef },
156
+ surfaceOp: null, sourceEventSeqs: [] });
157
+ index.set(message.id, [source]);
158
+ }
159
+ return { messageId: message.id, sourceId: source.id, inboxRef, messageHash: hash(message) };
160
+ });
161
+ });
162
+ }
163
+
164
+ /** Append/replay a contiguous prefix or its known continuation, atomically. */
165
+ ingestSession({ sessionId, events }) {
166
+ const batch = eventBatch(sessionId, events);
167
+ return this.transaction(() => {
168
+ const tail = this.list('dsh_events', { where: { sessionId }, order: 'desc', limit: 1 })[0];
169
+ let next = (tail?.seq ?? -1) + 1, inserted = 0, reused = 0;
170
+ const sourceIds = [];
171
+ const sources = sourceIndex(this.listAll('events', { where: { sessionId } }));
172
+ for (const event of batch) {
173
+ const id = eventId(sessionId, event.seq), existing = this.get('dsh_events', id);
174
+ if (!existing && event.seq !== next)
175
+ throw new DomainError('event_sequence_gap', 'Import the missing original DSH events first.');
176
+ const sha256 = hash(event);
177
+ this.immutable('dsh_events', { id, sessionId, seq: event.seq, type: event.type, time: event.time, sha256 });
178
+ const projection = sourceProjection(sessionId, event, sha256);
179
+ if (projection) {
180
+ const previous = sources.get(projection.message.id) || [];
181
+ const inbox = previous.find(row => row.provenance.sourceRef.type === 'agent/inbox/spliced');
182
+ const nativeUser = projection.message.role === 'user' && projection.message.source.kind === 'user';
183
+ const originalInput = previous.some(row => row.message.role === 'user' && row.message.source.kind === 'user');
184
+ if (nativeUser || originalInput || inbox) sameMessage(previous, projection.message);
185
+ // An already stored formal source retains its original event identity.
186
+ // New formal admission reuses its prepared original, not a second copy.
187
+ const direct = this.get('events', id);
188
+ const source = direct ? this.immutable('events', projection)
189
+ : inbox || (nativeUser && previous[0]) || this.immutable('events', projection);
190
+ if (!previous.some(row => row.id === source.id)) sources.set(projection.message.id, [...previous, source]);
191
+ sourceIds.push(source.id);
192
+ }
193
+ if (existing) reused++;
194
+ else { inserted++; next++; }
195
+ }
196
+ return { inserted, reused, sourceIds };
197
+ });
198
+ }
199
+
200
+ /** Caller must have confirmed DSH flush === true; this never calls a model. */
201
+ settleTurn({ sessionId, turn, events }) {
202
+ if (!Number.isSafeInteger(turn) || turn < 0)
203
+ throw new DomainError('invalid_turn', 'A nonnegative DSH turn number is required.', 400);
204
+ const batch = eventBatch(sessionId, events);
205
+ if (!batch.length || batch[0].seq !== 0)
206
+ throw new DomainError('incomplete_history', 'Settlement requires the complete original event prefix.');
207
+ return this.transaction(() => {
208
+ const imported = this.ingestSession({ sessionId, events: batch });
209
+ const sourceIds = new Map(batch.filter(event => MESSAGE_TYPES.has(event.type))
210
+ .map((event, index) => [event.seq, imported.sourceIds[index]]));
211
+ const analyzed = analyzeTurn(sessionId, turn, batch, sourceIds);
212
+ if (analyzed.reason) return { status: 'not_deliverable', reason: analyzed.reason };
213
+ const { start, end, final, inputEventIds, sourceEventIds, background } = analyzed;
214
+ const rounds = batch.filter(event => event.seq > start.seq && event.seq < end.seq &&
215
+ event.type === 'user/message' && event.data.source?.kind === 'goal' && event.data.source.round > 0);
216
+ let nativeContinuation;
217
+ if (rounds.length) {
218
+ const snapshots = this.listAll('context_snapshots', { where: { sessionId, turn } });
219
+ nativeContinuation = snapshots.length === 1 ? snapshots[0].material.nativeContinuation : null;
220
+ if (!nativeContinuation || rounds.length !== 1 || nativeContinuation.messageId !== rounds[0].data.id ||
221
+ nativeContinuation.sessionId !== sessionId || nativeContinuation.turn !== turn ||
222
+ nativeContinuation.goalId !== rounds[0].data.source.goalId ||
223
+ nativeContinuation.revision !== rounds[0].data.source.revision ||
224
+ nativeContinuation.round !== rounds[0].data.source.round)
225
+ return { status: 'not_deliverable', reason: 'unproven_native_continuation' };
226
+ assertNativeContinuationIntegrity(this.storage, nativeContinuation);
227
+ }
228
+ const id = turnId(sessionId, turn), reused = this.get('deliveries', id) !== null;
229
+ const delivery = this.immutable('deliveries', {
230
+ id, kind: 'formal_reply', taskOutcome: 'not_assessed', conversationId: sessionId, sessionId, turn,
231
+ eventId: eventId(sessionId, final.seq), inputEventIds, sourceEventIds,
232
+ startSeq: start.seq, throughSeq: end.seq,
233
+ endEventId: eventId(sessionId, end.seq),
234
+ occurredAt: new Date(end.time).toISOString(), backgroundAtTurnEnd: background,
235
+ ...(nativeContinuation ? { nativeContinuation } : {}),
236
+ });
237
+ const learningId = `learn:${hash([sessionId, turn])}`;
238
+ let learning = this.get('learning_pending', learningId);
239
+ if (learning && (learning.deliveryId !== delivery.id || learning.throughSeq !== end.seq))
240
+ throw new DomainError('learning_identity_conflict', 'Learning work is bound to another delivery.');
241
+ learning ||= this.storage.save('learning_pending', {
242
+ id: learningId, kind: 'delivery_learning', status: 'pending', sessionId, conversationId: sessionId,
243
+ turn, deliveryId: delivery.id, throughSeq: end.seq, sourceEventIds,
244
+ });
245
+ return { status: 'delivered', delivery, learning, reused };
246
+ });
247
+ }
248
+ }
249
+
250
+ function analyzeTurn(sessionId, turn, events, sourceIds) {
251
+ const starts = events.filter(event => event.type === 'turn/start' && event.data.turn === turn);
252
+ const ends = events.filter(event => event.type === 'turn/end' && event.data.turn === turn);
253
+ if (starts.length !== 1 || ends.length !== 1 || starts[0].seq >= ends[0].seq)
254
+ return { reason: 'incomplete_turn' };
255
+ const start = starts[0], end = ends[0];
256
+ if (events.some(event => event.type === 'session/end-seed' && event.data.inherited === true && event.seq > end.seq))
257
+ return { reason: 'inherited_turn' };
258
+ if (end.data.reason?.kind !== 'completed') return { reason: `turn_${end.data.reason?.kind || 'unknown'}` };
259
+ const inside = events.filter(event => event.seq > start.seq && event.seq < end.seq);
260
+ if (inside.some(event => event.type === 'turn/start' || event.type === 'turn/end' ||
261
+ (event.data.turn !== undefined && event.data.turn !== turn))) return { reason: 'invalid_turn_boundary' };
262
+ let step = null;
263
+ for (const event of inside) {
264
+ if (event.type === 'step/start') {
265
+ if (step !== null || !Number.isSafeInteger(event.data.step) || event.data.step < 0)
266
+ return { reason: 'invalid_step_boundary' };
267
+ step = event.data.step;
268
+ } else if (event.type === 'step/end') {
269
+ if (step === null || step !== event.data.step) return { reason: 'invalid_step_boundary' };
270
+ step = null;
271
+ } else if (['assistant/message', 'assistant/attempt', 'tool/call', 'tool/result'].includes(event.type) &&
272
+ (step === null || event.data.step !== step)) return { reason: 'invalid_step_boundary' };
273
+ }
274
+ if (step !== null) return { reason: 'incomplete_step' };
275
+ const assistants = inside.filter(event => event.type === 'assistant/message');
276
+ const final = assistants.at(-1);
277
+ if (!final || final.data.interrupted === true || final.data.message.source.kind !== 'model')
278
+ return { reason: 'no_complete_assistant' };
279
+ if (final.data.message.content.some(block => block.type === 'tool-call')) return { reason: 'final_tool_call' };
280
+ if (!messageText(final.data.message).trim()) return { reason: 'empty_reply' };
281
+
282
+ // Surface replacements can hide an earlier reply; don't publish its old text.
283
+ let surface = [];
284
+ for (const event of events) {
285
+ if (event.seq >= end.seq) break;
286
+ if (!SURFACE_TYPES.has(event.type)) continue;
287
+ if (event.surfaceOp === 'append') surface.push(event.seq);
288
+ else if (event.surfaceOp?.op === 'replace') {
289
+ const first = surface.indexOf(event.surfaceOp.startSeq), last = surface.indexOf(event.surfaceOp.endSeq);
290
+ if (first < 0 || last < first) return { reason: 'invalid_surface' };
291
+ surface.splice(first, last - first + 1, event.seq);
292
+ } else return { reason: 'invalid_surface' };
293
+ }
294
+ if (!surface.includes(final.seq)) return { reason: 'reply_replaced' };
295
+ if (inside.some(event => event.seq > final.seq && event.type === 'user/message' && event.data.source.kind === 'user'))
296
+ return { reason: 'unanswered_user_input' };
297
+
298
+ const calls = new Map(), dispatched = new Set(), results = new Set();
299
+ for (const event of inside) {
300
+ if (event.type === 'assistant/message') {
301
+ for (const block of event.data.message.content.filter(block => block.type === 'tool-call')) {
302
+ if (typeof block.id !== 'string' || calls.has(block.id)) return { reason: 'invalid_tool_trace' };
303
+ calls.set(block.id, event.seq);
304
+ }
305
+ } else if (event.type === 'tool/call') {
306
+ if (typeof event.data.callId !== 'string' || dispatched.has(event.data.callId)) return { reason: 'invalid_tool_trace' };
307
+ dispatched.add(event.data.callId);
308
+ if (!calls.has(event.data.callId)) calls.set(event.data.callId, event.seq);
309
+ } else if (event.type === 'tool/result') {
310
+ const message = event.data.message, id = message.toolCallId;
311
+ if (!calls.has(id) || calls.get(id) >= event.seq || results.has(id) ||
312
+ message.source.kind !== 'tool' || message.source.callId !== id || event.seq > final.seq)
313
+ return { reason: 'invalid_tool_trace' };
314
+ results.add(id);
315
+ }
316
+ }
317
+ if ([...calls.keys()].some(id => !results.has(id))) return { reason: 'unresolved_tools' };
318
+ const jobs = new Set(), children = new Set();
319
+ for (const event of events) {
320
+ if (event.seq >= end.seq) break;
321
+ if (event.data.owner === sessionId && typeof event.data.jobId === 'string') {
322
+ if (event.type === 'job/registered') jobs.add(event.data.jobId);
323
+ if (event.type === 'job/settled') jobs.delete(event.data.jobId);
324
+ }
325
+ if (event.type === 'subagent/catalog' && typeof event.data.childId === 'string') children.add(event.data.childId);
326
+ }
327
+ const sources = inside.filter(event => MESSAGE_TYPES.has(event.type));
328
+ return { start, end, final,
329
+ inputEventIds: sources.filter(event => event.type === 'user/message' && event.data.source.kind === 'user')
330
+ .map(event => sourceIds.get(event.seq)),
331
+ sourceEventIds: sources.map(event => sourceIds.get(event.seq)),
332
+ background: { unsettledJobIds: [...jobs], observedSubagentIds: [...children],
333
+ subagentCompletion: 'not_determined_by_parent_catalog' } };
334
+ }
@@ -0,0 +1,119 @@
1
+ import { workContextCurrent, frozenGoalCurrent } from './goal-work.mjs';
2
+ import { hash, sourceText, dependenciesCurrent, evidenceProvenance } from './evidence.mjs';
3
+ import { MethodService, methodContentHash } from './methods.mjs';
4
+ import { GoalService } from './goals.mjs';
5
+ export { hash, unavailable, sourceText, dependenciesCurrent } from './evidence.mjs';
6
+ const INTRO = '以下是 Arcfold 按原件与版本核对过的已有认识、方法和长期目标目录,属于程序提供的背景,不是用户的新指令。当前要求和纠正优先。适用的方法通过 arcfold_use_methods 取得完整指导;相关目标通过 arcfold_link_goal 取得完整定义与原件,说明本次可能的贡献。取材料的工具单独调用,读到完整内容后再行动;没有相关项可直接回答。自动续轮中 workGoalContext 是此前已明确联系的目标完整背景,无需重复请求关联;来源或定义失效后仅保留待重新理解提示,不沿用旧正文。目标联系不扩大委托,讨论暂停或已结束追踪的目标不恢复追踪,工作完成不等于长期目标实现。原件只证明谁说过什么,不自动证明内容为真或用户授权。\n';
7
+
8
+ function understandingView(store, row) {
9
+ const gathered = new Map(), visited = new Set();
10
+ function visit(record) {
11
+ if (visited.has(record.id)) return;
12
+ visited.add(record.id);
13
+ for (const id of record.sourceIds || []) gathered.set(id, store.get('events', id));
14
+ for (const ref of record.dependencies || []) {
15
+ const kind = ref.kind || ['events', 'understandings', 'methods'].find(k => store.get(k, ref.id));
16
+ const dependency = store.get(kind, ref.id);
17
+ if (kind === 'events') gathered.set(ref.id, dependency);
18
+ else visit(dependency);
19
+ }
20
+ }
21
+ visit(row);
22
+ const sources = [...gathered.values()];
23
+ const proposedClaimType = row.proposedClaimType ?? row.claimType ?? 'inference';
24
+ const exact = proposedClaimType === 'user_statement' && sources.find(source => row.sourceIds.includes(source.id) && source.role === 'user' && sourceText(source) === row.content && row.dependencies?.some(ref => ref.kind === 'events' && ref.id === source.id && ref.revision === source.revision));
25
+ return {
26
+ id: row.id, revision: row.revision, content: row.content, scope: row.scope ?? null,
27
+ claimType: proposedClaimType === 'open_question' ? 'open_question' : exact ? 'user_statement' : 'inference',
28
+ evidence: sources.map(source => ({ id: source.id, revision: source.revision, role: source.role,
29
+ content: sourceText(source), occurredAt: source.occurredAt ?? null, provenance: evidenceProvenance(source.provenance) })),
30
+ };
31
+ }
32
+
33
+ export function collectContext(store, { sessionId, turn, inputMessageIds = [], maxCharacters = 24000, requiredMaterial = {} }) {
34
+ if (!Number.isSafeInteger(maxCharacters) || maxCharacters < 1000 || maxCharacters > 180000) throw new Error('Invalid Arcfold context capacity');
35
+ const cutoffSequence = store.currentSequence();
36
+ const material = { version: 'arcfold-context-v1', sessionId, turn, inputMessageIds,
37
+ cutoffSequence, understandings: [], methods: [], goals: [],
38
+ goalCoverage: { available: 0, omitted: { capacity: 0, changed: 0 } },
39
+ methodCoverage: { available: 0, omitted: { capacity: 0, changed: 0 } },
40
+ coverage: { available: 0, omitted: { unavailable: 0, capacity: 0, changed: 0 } },
41
+ authority: 'revisable background with original evidence: user_statement is verbatim user text; inference and open_question are assistant interpretations; neither is proof of truth or action permission' };
42
+ // Only these proven complete additions can reserve capacity. Callers cannot
43
+ // replace version, session identity, cutoff or coverage metadata.
44
+ for (const key of ['nativeContinuation', 'workGoalContext', 'executionStage'])
45
+ if (requiredMaterial[key] !== undefined) material[key] = requiredMaterial[key];
46
+ if (contextMessage(material).content[0].text.length > maxCharacters)
47
+ throw new Error('Complete native delegation and context exceed the material capacity; nothing was truncated');
48
+ const goals = new GoalService({ business: { storage: store } }).catalog({ sessionId, cutoffSequence, inputMessageIds });
49
+ const addGoal = goal => {
50
+ material.goalCoverage.available++;
51
+ if (INTRO.length + JSON.stringify({ ...material, goals: [...material.goals, goal] }).length + 64 > maxCharacters)
52
+ material.goalCoverage.omitted.capacity++;
53
+ else material.goals.push(goal);
54
+ };
55
+ // A user's exact selected discussion gets capacity before inferred background.
56
+ goals.filter(goal => goal.explicitDiscussion).forEach(addGoal);
57
+ const candidates = store.listAll('understandings').filter(row => row.status === 'active' && (!row.scope?.conversationId || row.scope.conversationId === sessionId));
58
+ for (const row of candidates) {
59
+ if (!dependenciesCurrent(store, row, cutoffSequence)) { material.coverage.omitted.unavailable++; continue; }
60
+ material.coverage.available++;
61
+ const view = understandingView(store, row);
62
+ if (INTRO.length + JSON.stringify({ ...material, understandings: [...material.understandings, view] }).length + 64 > maxCharacters) {
63
+ material.coverage.omitted.capacity++;
64
+ } else material.understandings.push(view);
65
+ }
66
+ goals.filter(goal => !goal.explicitDiscussion).forEach(addGoal);
67
+ const methods = new MethodService({ business: { storage: store } }).catalog({ sessionId, cutoffSequence });
68
+ for (const method of methods) {
69
+ material.methodCoverage.available++;
70
+ if (INTRO.length + JSON.stringify({ ...material, methods: [...material.methods, method] }).length + 64 > maxCharacters)
71
+ material.methodCoverage.omitted.capacity++;
72
+ else material.methods.push(method);
73
+ }
74
+ if (INTRO.length + JSON.stringify(material).length > maxCharacters) throw new Error('Arcfold context metadata exceeds capacity');
75
+ return material;
76
+ }
77
+
78
+ export function refreshContext(store, frozen) {
79
+ const material = structuredClone(frozen);
80
+ // Refresh display metadata on a copy, without rewriting historical snapshots
81
+ // or treating a provider's replay envelope as original conversation text.
82
+ for (const source of [...material.understandings.flatMap(row => row.evidence || []),
83
+ ...(material.workGoalContext?.sources || [])])
84
+ source.provenance = evidenceProvenance(source.provenance);
85
+ if (material.workGoalContext && !workContextCurrent(store, material.workGoalContext)) delete material.workGoalContext;
86
+ material.understandings = material.understandings.filter(shown => {
87
+ const current = store.get('understandings', shown.id);
88
+ const valid = current?.revision === shown.revision && dependenciesCurrent(store, current, material.cutoffSequence);
89
+ if (!valid) material.coverage.omitted.changed++;
90
+ return valid;
91
+ });
92
+ material.methodCoverage ??= { available: 0, omitted: { capacity: 0, changed: 0 } };
93
+ material.methods = (material.methods || []).filter(shown => {
94
+ const current = store.get('methods', shown.id);
95
+ // Remaining trials are a snapshot, not an entitlement. A quota-only change
96
+ // must not abort a normal answer; reserve() checks the current count atomically.
97
+ const valid = ['trial', 'active'].includes(current?.status) && current.revision === shown.revision &&
98
+ methodContentHash(current) === shown.contentHash && dependenciesCurrent(store, current, material.cutoffSequence);
99
+ if (!valid) material.methodCoverage.omitted.changed++;
100
+ return valid;
101
+ });
102
+ material.goalCoverage ??= { available: 0, omitted: { capacity: 0, changed: 0 } };
103
+ const goals = new Map(new GoalService({ business: { storage: store } }).catalog({
104
+ sessionId: frozen.sessionId, cutoffSequence: frozen.cutoffSequence, inputMessageIds: frozen.inputMessageIds,
105
+ }).map(goal => [goal.id, goal]));
106
+ material.goals = (material.goals || []).filter(shown => {
107
+ const current = goals.get(shown.id);
108
+ const valid = current && hash(current) === hash(shown) || frozenGoalCurrent(store, shown, material.cutoffSequence);
109
+ if (!valid) material.goalCoverage.omitted.changed++;
110
+ return valid;
111
+ });
112
+ return material;
113
+ }
114
+
115
+ export function contextMessage(material) {
116
+ const contextId = 'arcfold-context-' + hash(material);
117
+ return { id: contextId, role: 'user', source: { kind: 'arcfold/context', contextId }, content: [{ type: 'text',
118
+ text: INTRO + JSON.stringify(material) }] };
119
+ }
@@ -0,0 +1,58 @@
1
+ import { isAbsolute } from 'node:path';
2
+ import { BudgetLedger } from './budget.mjs';
3
+ import { DeepSeekTransport } from './deepseek-transport.mjs';
4
+ import { getRuntimeGuard } from './runtime-guard.mjs';
5
+ import { nativeRequestIdentity, ownEvidenceDirectory, assertNativeRequestCurrent } from './native-request-authority.mjs';
6
+ export { nativeRequestIdentity } from './native-request-authority.mjs';
7
+
8
+ export const name = 'arcfold-deepseek-budget';
9
+ export const inject = ['arcfoldBusiness', 'arcfoldBridge', 'sessions', 'llm'];
10
+ export async function apply(ctx, config = {}) {
11
+ if (!isAbsolute(config.ledgerPath || '') || !isAbsolute(config.artifactDirectory || ''))
12
+ throw new Error('Explicit absolute paths to the existing ledger and this host evidence directory are required');
13
+ if (config.provider !== 'deepseek-official') throw new Error('This profile has one explicitly metered DeepSeek route');
14
+ if (!['limited', 'web-unlimited'].includes(config.scope) || !config.authorizationRef || !Number.isFinite(config.ceilingCny))
15
+ throw new Error('An explicit existing budget authorization and host scope are required');
16
+ const lease = await ownEvidenceDirectory(config.artifactDirectory);
17
+ let ledger, transport;
18
+ let closing;
19
+ const close = () => closing ??= (async () => {
20
+ try { await transport?.close(); }
21
+ finally { try { ledger?.close(); } finally { await lease.close(); } }
22
+ })();
23
+ ctx.effect(() => close, 'close owned transport before the ledger and kernel lock');
24
+ try {
25
+ ledger = new BudgetLedger(config.ledgerPath, {
26
+ ceilingCny: config.ceilingCny, authorizationRef: config.authorizationRef, scope: config.scope,
27
+ webAuthorization: config.webAuthorization, create: false,
28
+ });
29
+ transport = new DeepSeekTransport({ ledger, artifactDirectory: config.artifactDirectory,
30
+ provider: config.provider, modelPolicies: config.modelPolicies, endpoint: config.endpoint,
31
+ allowLocalTestEndpoint: config.allowLocalTestEndpoint === true,
32
+ timeoutMs: config.timeoutMs,
33
+ requestIdentity: options => nativeRequestIdentity(ctx.arcfoldBusiness, ctx.sessions, options),
34
+ beforeDispatch: options => {
35
+ getRuntimeGuard(ctx)?.assertDispatch(options);
36
+ return assertNativeRequestCurrent(ctx, options);
37
+ },
38
+ });
39
+ // This lock excludes another process using these artifacts. The original
40
+ // shared ledger may still be used by the independent web host.
41
+ await transport.recover({ exclusive: true });
42
+ ctx.on('llm/stream', (options, next) => {
43
+ if (options.provider !== config.provider) return next();
44
+ transport.validateInput(options); // before the host can degrade attachments
45
+ return next();
46
+ }, { prepend: true });
47
+ const policies = structuredClone(transport.modelPolicies);
48
+ for (const policy of Object.values(policies)) {
49
+ Object.freeze(policy.prices); Object.freeze(policy.reasoningEfforts); Object.freeze(policy);
50
+ }
51
+ ctx.provide('arcfoldDeepSeekTransport', Object.freeze({
52
+ summary: () => ledger.summary(),
53
+ createTransport: options => transport.createTransport(options),
54
+ configuration: Object.freeze({ provider: config.provider, endpoint: transport.endpoint,
55
+ modelPolicies: Object.freeze(policies) }),
56
+ }));
57
+ } catch (error) { await close(); throw error; }
58
+ }
@@ -0,0 +1,57 @@
1
+ import { dirname } from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { assertUsableApiKey, LlmError } from '@deepseek-ai/dsh-llm';
4
+ import { catalogModelInfo, registerDeepSeekProvider } from '@deepseek-ai/dsh-llm-deepseek';
5
+ import { resolveAdapterOptions } from '@deepseek-ai/dsh-llm-deepseek-api-key';
6
+ import { launchEnvironmentOf } from '@deepseek-ai/dsh-launch-environment';
7
+ import { inspectDeepSeekPatch } from '../scripts/patch-deepseek-transport.mjs';
8
+
9
+ const TRANSPORT_CONTRACT = 'deepseek-messages-create-transport-v1';
10
+
11
+ export const name = 'arcfold-deepseek-provider';
12
+ export const inject = ['llm', 'credentials', 'arcfoldDeepSeekTransport'];
13
+ export function apply(ctx, config = {}) {
14
+ // Check the loaded function as well as disk: patching a file after an earlier
15
+ // import must not make the cached original adapter look safe to register.
16
+ if (registerDeepSeekProvider.transportSeam !== TRANSPORT_CONTRACT)
17
+ throw new Error('The loaded DeepSeek provider lacks the required transport interface');
18
+ const packageDir = dirname(fileURLToPath(import.meta.resolve('@deepseek-ai/dsh-llm-deepseek/package.json')));
19
+ if (inspectDeepSeekPatch(packageDir).state !== 'patched')
20
+ throw new Error('The exact supported DeepSeek provider patch is required');
21
+ const service = ctx.arcfoldDeepSeekTransport;
22
+ const { provider, endpoint, modelPolicies } = service.configuration;
23
+ if (provider !== 'deepseek-official' || !Object.keys(modelPolicies).length)
24
+ throw new Error('An explicit metered DeepSeek model catalog is required');
25
+ if (typeof config.apiKeyRef !== 'string' || !config.apiKeyRef ||
26
+ !Number.isSafeInteger(config.maxTokens) || config.maxTokens < 1 ||
27
+ !['off', 'low', 'high', 'max'].includes(config.reasoningEffort))
28
+ throw new Error('A credential reference, output limit and reasoning effort are required');
29
+ const models = Object.entries(modelPolicies).map(([id, policy]) => {
30
+ if (config.maxTokens > (policy.requestMaxTokens ?? policy.maxOutputTokens) ||
31
+ !policy.reasoningEfforts.includes(config.reasoningEffort))
32
+ throw new Error('Provider defaults exceed a frozen model accounting policy');
33
+ return { id, name: id, inputModalities: ['text'], contextWindow: policy.contextWindow,
34
+ maxTokens: config.maxTokens };
35
+ });
36
+ // This deployment generation is fixed. A different route/catalog requires a
37
+ // plugin reload; the user-selected model remains an ordinary DSH option.
38
+ const connection = resolveAdapterOptions({
39
+ baseURL: endpoint.slice(0, -'/v1/messages'.length), apiKeyEnv: config.apiKeyRef,
40
+ models, maxTokens: config.maxTokens, reasoningEffort: config.reasoningEffort,
41
+ retryPolicy: { mode: 'normal', maxRetries: 0 },
42
+ }, launchEnvironmentOf(ctx));
43
+ for (const model of connection.models) Object.freeze(model);
44
+ Object.freeze(connection.models); Object.freeze(connection);
45
+ registerDeepSeekProvider(ctx, provider, {
46
+ providerName: 'DeepSeek', options: () => connection, connect: async () => connection,
47
+ createTransport: options => service.createTransport(options),
48
+ // Resolve secrets only inside the official credential service. They remain
49
+ // in memory for the request and never enter Arcfold config or evidence.
50
+ resolveAuth: async current => {
51
+ const credential = await ctx.credentials.resolve(current.apiKeyEnv);
52
+ if (!credential) throw new LlmError('Configure the DeepSeek credential in the host Models page', 'MISSING_CREDENTIAL');
53
+ return { headers: { 'x-api-key': assertUsableApiKey(credential.value, name, current.apiKeyEnv) } };
54
+ },
55
+ discoverModels: async route => connection.models.map(model => catalogModelInfo(route, model)),
56
+ });
57
+ }