@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,18 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { realpath, stat } from 'node:fs/promises';
|
|
3
|
+
import { isAbsolute } from 'node:path';
|
|
4
|
+
|
|
5
|
+
export const name = 'arcfold-runtime-workspace';
|
|
6
|
+
export const inject = ['workspaceRegistry', 'sessions'];
|
|
7
|
+
|
|
8
|
+
/** Establish the native first-use workspace before its browser API is exposed.
|
|
9
|
+
* The registry owns persistence and deliberately preserves existing selections,
|
|
10
|
+
* session history, and a user's deletion of the automatic workspace. */
|
|
11
|
+
export async function apply(ctx, config = {}) {
|
|
12
|
+
assert.ok(typeof config.workspace === 'string' && isAbsolute(config.workspace), 'An explicit workspace is required');
|
|
13
|
+
const path = await realpath(config.workspace);
|
|
14
|
+
assert.equal(path, config.workspace, 'The deployment workspace must be canonical');
|
|
15
|
+
assert.ok((await stat(path)).isDirectory(), 'The deployment workspace must exist');
|
|
16
|
+
const initial = await ctx.workspaceRegistry.initializeDefault(async () => path);
|
|
17
|
+
ctx.provide('arcfoldRuntimeWorkspace', Object.freeze({ requestedPath: path, defaultWorkspaceId: initial?.id ?? null }));
|
|
18
|
+
}
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import { captureWorks, workId, workState, automaticWorkContext } from './goal-work.mjs';
|
|
2
|
+
import { freezeNativeContinuation, assertNativeContinuationCurrent, assertNativeContinuationStored, assertNativeContinuationIntegrity } from './native-continuation.mjs';
|
|
3
|
+
import { collectContext, contextMessage, hash, refreshContext } from './context-material.mjs';
|
|
4
|
+
import { sourceAvailable } from './evidence.mjs';
|
|
5
|
+
import { canonicalJson } from './store.mjs';
|
|
6
|
+
import { MethodService } from './methods.mjs';
|
|
7
|
+
import { METHOD_CONTEXT, methodContextMessage, eligibleMethodBundle, methodTurn } from './method-context.mjs';
|
|
8
|
+
import { GoalService } from './goals.mjs';
|
|
9
|
+
import { UnderstandingService } from './understandings.mjs';
|
|
10
|
+
import { GOAL_CONTEXT, goalContextMessage, eligibleGoalBundle } from './goal-context.mjs';
|
|
11
|
+
|
|
12
|
+
export class SessionBridge {
|
|
13
|
+
constructor({ business, methods = new MethodService({ business }), goals = new GoalService({ business }), understandings = new UnderstandingService({ business, goals }), sessions, persistence, logger = console, maxContextCharacters = 24000 }) {
|
|
14
|
+
Object.assign(this, { business, sessions, persistence, logger, maxContextCharacters });
|
|
15
|
+
this.methods = methods;
|
|
16
|
+
this.goals = goals;
|
|
17
|
+
this.understandings = understandings;
|
|
18
|
+
this.pending = new Map();
|
|
19
|
+
this.closed = false;
|
|
20
|
+
}
|
|
21
|
+
get storage() { return this.business.storage; }
|
|
22
|
+
isBound(sessionId) { return typeof sessionId === 'string' && this.storage.get('host_bindings', sessionId)?.role === 'main'; }
|
|
23
|
+
|
|
24
|
+
// A reference to the existing host, not another goal/activation registry.
|
|
25
|
+
useNativeRuntime(root) {
|
|
26
|
+
if (this.nativeRoot && this.nativeRoot !== root) throw new Error('Arcfold native host identity changed');
|
|
27
|
+
this.nativeRoot = root;
|
|
28
|
+
this.goals.nativeRoot = root;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// The official synchronous tools.guard runs after pre-execute/approval.
|
|
32
|
+
// No separate terminal flag: use only this turn's frozen proof and live goal.
|
|
33
|
+
nativeContinuationToolDenial(exec) {
|
|
34
|
+
const agent = exec.agent, session = agent?.session, events = session?.snapshotEvents?.();
|
|
35
|
+
const start = events?.findLast(event => event.type === 'turn/start');
|
|
36
|
+
if (!start) return;
|
|
37
|
+
const snapshots = this.storage.listAll('context_snapshots', { where: { sessionId: session.id, turn: start.data.turn } });
|
|
38
|
+
const proof = snapshots.find(row => row.material.nativeContinuation)?.material.nativeContinuation;
|
|
39
|
+
if (!proof) return; // A later ordinary human turn is not the prior automatic round.
|
|
40
|
+
try {
|
|
41
|
+
if (snapshots.length !== 1 || this.nativeRoot?.get('agents')?.get(agent.id) !== agent ||
|
|
42
|
+
agent.status !== 'running' || events.some(event => event.seq > start.seq && event.type === 'turn/end'))
|
|
43
|
+
throw new Error('Native continuation tool no longer belongs to its current turn');
|
|
44
|
+
const historical = this.work?.historicalContinuation(agent);
|
|
45
|
+
if (historical) assertNativeContinuationIntegrity(this.storage, proof);
|
|
46
|
+
else assertNativeContinuationStored(this.storage, proof);
|
|
47
|
+
const goal = this.nativeRoot.get('goals').get(agent);
|
|
48
|
+
if (!historical && (!goal || goal.id !== proof.goalId || goal.revision !== proof.revision || goal.phase !== 'active' ||
|
|
49
|
+
goal.activation !== 'armed' || goal.roundsStarted !== proof.round))
|
|
50
|
+
return 'This automatic goal round no longer authorizes tools. After completion or blocking, provide only the final explanation.';
|
|
51
|
+
const callId = exec.rootCallId ?? exec.callId;
|
|
52
|
+
const call = events.findLast(event => event.type === 'tool/call' && event.data.callId === callId);
|
|
53
|
+
const assistant = call && events.findLast(event => event.seq < call.seq && event.type === 'assistant/message' &&
|
|
54
|
+
event.data.turn === call.data.turn && event.data.step === call.data.step);
|
|
55
|
+
if (!call || call.data.turn !== start.data.turn || !assistant ||
|
|
56
|
+
assistant.data.message.role !== 'assistant' || assistant.data.message.source?.kind !== 'model')
|
|
57
|
+
throw new Error('Native continuation tool has no current model call original');
|
|
58
|
+
const batch = assistant.data.message.content.filter(block => block.type === 'tool-call');
|
|
59
|
+
const terminal = batch.some(block => {
|
|
60
|
+
if (block.name !== 'update_goal') return false;
|
|
61
|
+
try { return ['complete', 'blocked'].includes(JSON.parse(block.arguments).action); } catch { return false; }
|
|
62
|
+
});
|
|
63
|
+
if (terminal && batch.length !== 1)
|
|
64
|
+
return 'Call update_goal complete or blocked alone. A terminal transition cannot share a tool batch with further actions.';
|
|
65
|
+
} catch (error) { return error.message; }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async durablePrefix(session) {
|
|
69
|
+
// Capture BEFORE the awaited flush: later events are not covered by this barrier.
|
|
70
|
+
const events = session.snapshotEvents();
|
|
71
|
+
if (await this.sessions.flush(session) !== true) throw new Error('Arcfold requires a participating durable session writer');
|
|
72
|
+
return events;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
ingestAndSettle(sessionId, events) {
|
|
76
|
+
this.business.ingestSession({ sessionId, events });
|
|
77
|
+
captureWorks(this.storage, sessionId, events);
|
|
78
|
+
const settled = [];
|
|
79
|
+
for (const event of events) if (event.type === 'turn/end') {
|
|
80
|
+
// The full durable prefix also proves fork inheritance. BusinessStore
|
|
81
|
+
// restricts this delivery's sources to its own turn/end internally.
|
|
82
|
+
const result = this.business.settleTurn({ sessionId, turn: event.data.turn, events });
|
|
83
|
+
settled.push(result);
|
|
84
|
+
const starts = events.filter(row => row.type === 'turn/start' && row.data.turn === event.data.turn);
|
|
85
|
+
const ends = events.filter(row => row.type === 'turn/end' && row.data.turn === event.data.turn);
|
|
86
|
+
const inherited = events.some(row => row.type === 'session/end-seed' && row.data.inherited === true && row.seq > event.seq);
|
|
87
|
+
if (starts.length === 1 && ends.length === 1 && starts[0].seq < event.seq && !inherited) {
|
|
88
|
+
const outcome = ({ completed: 'completed', aborted: 'cancelled', error: 'failed', interrupted: 'interrupted' })[event.data.reason?.kind] ?? 'unknown';
|
|
89
|
+
this.methods.settleTurn({ sessionId, turn: event.data.turn, startSeq: starts[0].seq,
|
|
90
|
+
outcome, deliveryId: result.delivery?.id ?? null });
|
|
91
|
+
this.goals.settleTurn({ sessionId, turn: event.data.turn, startSeq: starts[0].seq,
|
|
92
|
+
outcome, deliveryId: result.delivery?.id ?? null });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return settled;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async reconcile(session) {
|
|
99
|
+
if (!this.isBound(session.id)) return [];
|
|
100
|
+
let events;
|
|
101
|
+
try { events = await this.durablePrefix(session); }
|
|
102
|
+
catch (error) {
|
|
103
|
+
// The host may close its writer before our queued turn-end observer runs.
|
|
104
|
+
// Keep that failure, then reconcile only actual persisted bytes. A missing
|
|
105
|
+
// durable end remains unresolved; a live/synthesized end is not evidence.
|
|
106
|
+
this.recordFailure(session.id, error);
|
|
107
|
+
events = await this.readPersisted(session.id);
|
|
108
|
+
}
|
|
109
|
+
return this.ingestAndSettle(session.id, events);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async readPersisted(sessionId) {
|
|
113
|
+
const handle = await this.persistence.open(sessionId, 'read');
|
|
114
|
+
try { return (await handle.read()).events; }
|
|
115
|
+
finally { await handle.close(); }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
recordFailure(sessionId, error) {
|
|
119
|
+
const id = 'bridge-failure-' + hash({ sessionId, message: String(error?.message || error) });
|
|
120
|
+
if (!this.storage.get('bridge_failures', id)) this.storage.save('bridge_failures', {
|
|
121
|
+
id, sessionId, message: String(error?.message || error), operation: 'business_reconciliation',
|
|
122
|
+
authority: 'does not change the DSH execution result',
|
|
123
|
+
});
|
|
124
|
+
this.logger.warn?.(`Arcfold could not reconcile ${sessionId}: ${String(error?.message || error)}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
assertRequestCurrent(options, events = null) {
|
|
128
|
+
const liveAgent = this.nativeRoot?.get('agents')?.get(options.sessionId);
|
|
129
|
+
if (liveAgent) this.work?.assertCurrent(liveAgent);
|
|
130
|
+
const messages = options.messages.filter(message => message.source?.kind === 'arcfold/context');
|
|
131
|
+
if (messages.length !== 1) throw new Error('Arcfold request must contain exactly one current context snapshot');
|
|
132
|
+
const message = messages[0];
|
|
133
|
+
const snapshots = this.storage.listAll('context_snapshots', { where: { sessionId: options.sessionId, contextMessageId: message.id } });
|
|
134
|
+
if (snapshots.length !== 1 || hash(contextMessage(snapshots[0].material)) !== hash(message)) throw new Error('Arcfold request context is not the saved material');
|
|
135
|
+
if (hash(refreshContext(this.storage, snapshots[0].material)) !== hash(snapshots[0].material)) {
|
|
136
|
+
const error = new Error('Arcfold background changed before model dispatch; this request was not sent');
|
|
137
|
+
error.code = 'ARCFOLD_CONTEXT_CHANGED';
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
this.assertClaimedInputsEntered(options, snapshots[0], events);
|
|
141
|
+
const continuation = snapshots[0].material.nativeContinuation;
|
|
142
|
+
if (continuation) {
|
|
143
|
+
const agent = this.nativeRoot?.get('agents')?.get(options.sessionId);
|
|
144
|
+
assertNativeContinuationCurrent({ store: this.storage, proof: continuation,
|
|
145
|
+
events: events ?? this.sessions.get(options.sessionId)?.snapshotEvents() ?? [],
|
|
146
|
+
agent, goal: agent && this.nativeRoot.get('goals')?.get(agent), messages: options.messages,
|
|
147
|
+
historical: agent && this.work?.historicalContinuation(agent) });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Preparing from a durable inbox original is not proof that the host admitted
|
|
152
|
+
// it. Check the actual native turn and the assembled request before dispatch.
|
|
153
|
+
assertClaimedInputsEntered(options, snapshot, events = null) {
|
|
154
|
+
const claims = snapshot.claimedInputs || [];
|
|
155
|
+
if (!claims.length) return;
|
|
156
|
+
const native = events ?? this.sessions.get(options.sessionId)?.snapshotEvents();
|
|
157
|
+
const reject = () => { throw Object.assign(new Error('Arcfold claimed input did not enter this native turn unchanged; this request was not sent'),
|
|
158
|
+
{ code: 'ARCFOLD_CONTEXT_CHANGED' }); };
|
|
159
|
+
if (!native) reject();
|
|
160
|
+
const starts = native.filter(event => event.type === 'turn/start' && event.data.turn === snapshot.turn);
|
|
161
|
+
if (starts.length !== 1 || native.some(event => event.seq > starts[0].seq && event.type === 'turn/end')) reject();
|
|
162
|
+
for (const claim of claims) {
|
|
163
|
+
const source = this.storage.get('events', claim.sourceId), ref = claim.inboxRef;
|
|
164
|
+
if (!sourceAvailable(this.storage, source) || source.sessionId !== options.sessionId ||
|
|
165
|
+
source.message?.id !== claim.messageId || source.message.role !== 'user' ||
|
|
166
|
+
source.message.source?.kind !== 'user' || source.message.source.principal !== undefined ||
|
|
167
|
+
hash(JSON.parse(canonicalJson(source.message))) !== claim.messageHash ||
|
|
168
|
+
ref?.sessionId !== options.sessionId || ref.type !== 'agent/inbox/spliced' || !Number.isSafeInteger(ref.index)) reject();
|
|
169
|
+
const inbox = native.filter(event => event.seq === ref.seq && event.type === ref.type);
|
|
170
|
+
if (inbox.length !== 1 || hash(JSON.parse(canonicalJson(inbox[0]))) !== ref.sha256 ||
|
|
171
|
+
canonicalJson(inbox[0].data.inserted?.[ref.index] ?? null) !== canonicalJson(source.message)) reject();
|
|
172
|
+
const entered = native.filter(event => event.seq > starts[0].seq && event.type === 'user/message' && event.data.id === claim.messageId);
|
|
173
|
+
const sent = options.messages.filter(message => message.id === claim.messageId);
|
|
174
|
+
if (entered.length !== 1 || sent.length !== 1 || canonicalJson(entered[0].data) !== canonicalJson(source.message) ||
|
|
175
|
+
canonicalJson(sent[0]) !== canonicalJson(source.message)) reject();
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Synchronous so the paid sender can repeat this after its final awaited I/O,
|
|
180
|
+
// immediately before network dispatch. This does not claim a request was sent.
|
|
181
|
+
assertMethodRequestCurrent(options, events = null) {
|
|
182
|
+
const session = this.sessions.get(options.sessionId);
|
|
183
|
+
if (!session) throw new Error('Arcfold requires the current native session before dispatch');
|
|
184
|
+
const { position, step } = methodTurn(session);
|
|
185
|
+
const bundle = eligibleMethodBundle(this.methods, events ?? session.snapshotEvents(), position);
|
|
186
|
+
const expected = bundle.methods.length ? methodContextMessage(bundle) : null;
|
|
187
|
+
const provided = options.messages.filter(message => message.source?.kind === METHOD_CONTEXT);
|
|
188
|
+
if (provided.length !== (expected ? 1 : 0) || expected && hash(provided[0]) !== hash(expected)) {
|
|
189
|
+
const error = new Error('Arcfold method input changed or lacks a successful native selection result; this request was not sent');
|
|
190
|
+
error.code = 'ARCFOLD_METHOD_CONTEXT_CHANGED';
|
|
191
|
+
throw error;
|
|
192
|
+
}
|
|
193
|
+
return { bundle, expected, position, step };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
assertGoalRequestCurrent(options, events = null) {
|
|
197
|
+
const session = this.sessions.get(options.sessionId);
|
|
198
|
+
if (!session) throw new Error('Arcfold requires the current native session before dispatch');
|
|
199
|
+
const { position, step } = methodTurn(session);
|
|
200
|
+
const bundle = eligibleGoalBundle(this.goals, events ?? session.snapshotEvents(), position);
|
|
201
|
+
const expected = bundle.links.length ? goalContextMessage(bundle) : null;
|
|
202
|
+
const provided = options.messages.filter(message => message.source?.kind === GOAL_CONTEXT);
|
|
203
|
+
if (provided.length !== (expected ? 1 : 0) || expected && hash(provided[0]) !== hash(expected)) {
|
|
204
|
+
const error = new Error('Arcfold goal input changed or lacks a successful native selection result; this request was not sent');
|
|
205
|
+
error.code = 'ARCFOLD_GOAL_CONTEXT_CHANGED';
|
|
206
|
+
throw error;
|
|
207
|
+
}
|
|
208
|
+
return { bundle, expected, position, step };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
guardStream(options, next) {
|
|
212
|
+
if (!this.isBound(options.sessionId) || options.purpose !== undefined) return next();
|
|
213
|
+
const bridge = this;
|
|
214
|
+
return (async function* () {
|
|
215
|
+
options.signal?.throwIfAborted();
|
|
216
|
+
// Runs when the downstream model stream is first consumed, after awaited
|
|
217
|
+
// request preparation/checkpoints and before calling the provider chain.
|
|
218
|
+
const session = bridge.sessions.get(options.sessionId);
|
|
219
|
+
if (!session) throw new Error('Arcfold requires the current native session before dispatch');
|
|
220
|
+
const events = await bridge.durablePrefix(session);
|
|
221
|
+
options.signal?.throwIfAborted();
|
|
222
|
+
bridge.methods.reconcile();
|
|
223
|
+
bridge.goals.reconcile();
|
|
224
|
+
bridge.assertRequestCurrent(options, events);
|
|
225
|
+
const { position, step, bundle, expected } = bridge.assertMethodRequestCurrent(options, events);
|
|
226
|
+
const linked = bridge.assertGoalRequestCurrent(options, events);
|
|
227
|
+
if (expected) {
|
|
228
|
+
const requestIdentity = 'method-request-' + hash({ ...position, stepSeq: step.seq,
|
|
229
|
+
headerSeq: events.findLast(row => row.type === 'request/header')?.seq ?? null,
|
|
230
|
+
attempts: events.filter(row => row.seq > step.seq && row.type === 'assistant/attempt').map(row => row.seq) });
|
|
231
|
+
bridge.methods.recordInput({ adoptionIds: bundle.methods.map(row => row.adoptionId), ...position,
|
|
232
|
+
step: step.data.step, requestIdentity, inputHash: hash(expected) });
|
|
233
|
+
}
|
|
234
|
+
if (linked.expected) {
|
|
235
|
+
const requestIdentity = 'goal-request-' + hash({ ...linked.position, stepSeq: linked.step.seq,
|
|
236
|
+
headerSeq: events.findLast(row => row.type === 'request/header')?.seq ?? null,
|
|
237
|
+
attempts: events.filter(row => row.seq > linked.step.seq && row.type === 'assistant/attempt').map(row => row.seq) });
|
|
238
|
+
bridge.goals.recordInput({ linkIds: linked.bundle.links.map(row => row.linkId), ...linked.position,
|
|
239
|
+
step: linked.step.data.step, requestIdentity, inputHash: hash(linked.expected) });
|
|
240
|
+
}
|
|
241
|
+
yield* next();
|
|
242
|
+
})();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
observe(session, event) {
|
|
246
|
+
if (this.closed || event.type !== 'turn/end' || !this.isBound(session.id)) return;
|
|
247
|
+
const previous = this.pending.get(session.id) || Promise.resolve();
|
|
248
|
+
// A microtask leaves the synchronous session/event notification stack.
|
|
249
|
+
const task = previous.then(() => this.reconcile(session)).catch(error => this.recordFailure(session.id, error));
|
|
250
|
+
this.pending.set(session.id, task);
|
|
251
|
+
void task.finally(() => { if (this.pending.get(session.id) === task) this.pending.delete(session.id); }).catch(error => this.logger.error?.(error));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async prepare({ agent, messages, turn, step, signal }, decision) {
|
|
255
|
+
if (this.closed) throw new Error('Arcfold plugin is stopping');
|
|
256
|
+
if (decision.kind === 'reject' || step === 1 && decision.messages.length === 0) return decision;
|
|
257
|
+
signal.throwIfAborted();
|
|
258
|
+
const session = agent.session;
|
|
259
|
+
await this.pending.get(session.id);
|
|
260
|
+
const events = await this.durablePrefix(session);
|
|
261
|
+
signal.throwIfAborted();
|
|
262
|
+
this.methods.reconcile();
|
|
263
|
+
this.goals.reconcile();
|
|
264
|
+
const directInputs = messages.filter(message => message.role === 'user' && message.source?.kind === 'user');
|
|
265
|
+
for (const message of directInputs) {
|
|
266
|
+
const original = events.find(event => event.type === 'agent/inbox/spliced' && event.data.inserted?.some(input => input.id === message.id && hash(input) === hash(message)));
|
|
267
|
+
if (!original) throw new Error('Claimed user input has no matching durable inbox source');
|
|
268
|
+
}
|
|
269
|
+
let claimedInputs;
|
|
270
|
+
this.storage.transaction(() => {
|
|
271
|
+
const bound = this.storage.get('host_bindings', session.id);
|
|
272
|
+
if (!bound) this.storage.save('host_bindings', { id: session.id, sessionId: session.id, role: 'main', binding: 'trusted-arcfold-agent-preset' });
|
|
273
|
+
else if (bound.role !== 'main') throw new Error('Arcfold session has a conflicting role');
|
|
274
|
+
this.ingestAndSettle(session.id, events);
|
|
275
|
+
claimedInputs = this.business.ingestClaimedInputs({ sessionId: session.id, events,
|
|
276
|
+
messages: directInputs.filter(message => message.source.principal === undefined) });
|
|
277
|
+
});
|
|
278
|
+
// Keep original ingestion committed if an explicit replacement is stale.
|
|
279
|
+
this.understandings.applyClaimedCorrections({ sessionId: session.id, claims: claimedInputs, events });
|
|
280
|
+
const key = 'context-' + hash({ sessionId: session.id, turn });
|
|
281
|
+
const previous = this.storage.get('context_snapshots', key);
|
|
282
|
+
const nativeGoal = this.nativeRoot?.get('goals')?.get(agent);
|
|
283
|
+
const currentContinuation = freezeNativeContinuation({ store: this.storage, sessionId: session.id, turn,
|
|
284
|
+
messages, events, goal: nativeGoal, stage: this.work?.current(agent) });
|
|
285
|
+
if (previous?.material.nativeContinuation && currentContinuation &&
|
|
286
|
+
canonicalJson(previous.material.nativeContinuation) !== canonicalJson(currentContinuation))
|
|
287
|
+
throw new Error('Native continuation identity changed inside its turn');
|
|
288
|
+
const continuation = previous?.material.nativeContinuation ?? currentContinuation;
|
|
289
|
+
const claims = new Map((previous?.claimedInputs || []).map(claim => [claim.messageId, claim]));
|
|
290
|
+
for (const claim of claimedInputs) {
|
|
291
|
+
const prior = claims.get(claim.messageId);
|
|
292
|
+
if (prior && canonicalJson(prior) !== canonicalJson(claim)) throw new Error('Arcfold claimed input changed within its native turn');
|
|
293
|
+
claims.set(claim.messageId, claim);
|
|
294
|
+
}
|
|
295
|
+
claimedInputs = [...claims.values()];
|
|
296
|
+
const newInputIds = directInputs.map(message => message.id);
|
|
297
|
+
const reuse = previous && !newInputIds.some(id => !previous.material.inputMessageIds.includes(id));
|
|
298
|
+
const requiredMaterial = continuation ? { nativeContinuation: continuation } : {};
|
|
299
|
+
if (this.work) {
|
|
300
|
+
const currentStage = this.work.current(agent), stage = this.work.view(currentStage);
|
|
301
|
+
if (stage) requiredMaterial.executionStage = { ...stage, boundToTurn: this.work.boundToTurn(currentStage, turn) };
|
|
302
|
+
}
|
|
303
|
+
if (continuation && !reuse) {
|
|
304
|
+
const work = this.storage.get('goal_works', workId(session.id, continuation.goalId));
|
|
305
|
+
const background = work && automaticWorkContext(this.goals, work,
|
|
306
|
+
workState(this.storage, work.id), this.storage.currentSequence());
|
|
307
|
+
if (background) requiredMaterial.workGoalContext = background;
|
|
308
|
+
}
|
|
309
|
+
const material = reuse ? refreshContext(this.storage, previous.material)
|
|
310
|
+
: collectContext(this.storage, { sessionId: session.id, turn,
|
|
311
|
+
inputMessageIds: [...new Set([...(previous?.material.inputMessageIds || []), ...newInputIds])],
|
|
312
|
+
maxCharacters: this.maxContextCharacters, requiredMaterial });
|
|
313
|
+
if (requiredMaterial.executionStage) material.executionStage = requiredMaterial.executionStage;
|
|
314
|
+
const message = contextMessage(material);
|
|
315
|
+
if (!previous || hash(previous.material) !== hash(material) || canonicalJson(previous.claimedInputs || []) !== canonicalJson(claimedInputs)) this.storage.save('context_snapshots', {
|
|
316
|
+
id: key, sessionId: session.id, turn, material, contextMessageId: message.id, claimedInputs,
|
|
317
|
+
evidence: 'prepared-material; actual adoption requires matching native user/message and request',
|
|
318
|
+
}, { expectedRevision: previous?.revision ?? null });
|
|
319
|
+
signal.throwIfAborted();
|
|
320
|
+
const start = events.findLast(event => event.type === 'turn/start');
|
|
321
|
+
if (!start || start.data.turn !== turn || events.some(event => event.seq > start.seq && event.type === 'turn/end'))
|
|
322
|
+
throw new Error('Arcfold preparation requires the current open native turn');
|
|
323
|
+
const bundle = eligibleMethodBundle(this.methods, events, { sessionId: session.id, turn, startSeq: start.seq });
|
|
324
|
+
const methodMessage = bundle.methods.length ? methodContextMessage(bundle) : null;
|
|
325
|
+
const linked = eligibleGoalBundle(this.goals, events, { sessionId: session.id, turn, startSeq: start.seq });
|
|
326
|
+
const goalMessage = linked.links.length ? goalContextMessage(linked) : null;
|
|
327
|
+
const expected = new Map([['arcfold/context', message], [METHOD_CONTEXT, methodMessage], [GOAL_CONTEXT, goalMessage]]);
|
|
328
|
+
const present = new Set();
|
|
329
|
+
// Only our context nodes are retired; human originals and tool receipts stay.
|
|
330
|
+
for (const seq of [...session.surface.nodes]) {
|
|
331
|
+
const event = session.eventAt(seq);
|
|
332
|
+
const kind = event?.data.source?.kind;
|
|
333
|
+
if (event?.type !== 'user/message' || !expected.has(kind)) continue;
|
|
334
|
+
if (expected.get(kind) && !present.has(kind) && hash(event.data) === hash(expected.get(kind))) { present.add(kind); continue; }
|
|
335
|
+
session.append('user/message', {
|
|
336
|
+
id: 'arcfold-retired-' + hash({ sessionId: session.id, seq }), role: 'user',
|
|
337
|
+
source: { kind: 'arcfold/context-retired', replacedContextId: event.data.id },
|
|
338
|
+
content: [{ type: 'text', text: '此前的 Arcfold 背景已移出当前模型上下文;原始记录仍保留供历史审计。' }],
|
|
339
|
+
}, { surfaceOp: { op: 'replace', startSeq: seq, endSeq: seq }, sourceEventSeqs: [seq] });
|
|
340
|
+
}
|
|
341
|
+
// A deferred context can survive a failed tool. Reconstruct ours from saved
|
|
342
|
+
// selections plus successful native results instead of trusting that inbox.
|
|
343
|
+
const admitted = decision.messages.filter(row => !expected.has(row.source?.kind));
|
|
344
|
+
const additions = [...expected].filter(([kind, row]) => row && !present.has(kind)).map(([, row]) => row);
|
|
345
|
+
const lastInput = admitted.findLastIndex(row => messages.includes(row));
|
|
346
|
+
return { ...decision, messages: admitted.toSpliced(lastInput + 1, 0, ...additions) };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async recover() {
|
|
350
|
+
for (const binding of this.storage.listAll('host_bindings')) {
|
|
351
|
+
if (binding.role !== 'main') continue;
|
|
352
|
+
try {
|
|
353
|
+
const live = this.sessions.get(binding.sessionId);
|
|
354
|
+
if (live) { await this.reconcile(live); continue; }
|
|
355
|
+
this.ingestAndSettle(binding.sessionId, await this.readPersisted(binding.sessionId));
|
|
356
|
+
} catch (error) { this.recordFailure(binding.sessionId, error); }
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async close() {
|
|
361
|
+
this.closed = true;
|
|
362
|
+
await Promise.allSettled([...this.pending.values()]);
|
|
363
|
+
this.business.close();
|
|
364
|
+
}
|
|
365
|
+
}
|