@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,98 @@
|
|
|
1
|
+
import { isAbsolute } from 'node:path';
|
|
2
|
+
import { DomainError } from './store.mjs';
|
|
3
|
+
|
|
4
|
+
const KIND = 'learn_defaults', ID = 'web';
|
|
5
|
+
const CHOICE = ['provider', 'model', 'reasoningEffort'];
|
|
6
|
+
const EXECUTION = [...CHOICE, 'maxTokens', 'preset', 'cwd'];
|
|
7
|
+
const fail = (code, message) => { throw new DomainError(code, message); };
|
|
8
|
+
const choice = value => Object.fromEntries(CHOICE.map(key => [key, value?.[key]]));
|
|
9
|
+
|
|
10
|
+
function assertChoice(value) {
|
|
11
|
+
if (!value || !CHOICE.every(key => typeof value[key] === 'string' && value[key].trim()))
|
|
12
|
+
fail('learning_configuration_unavailable', '学习需要明确的模型提供方、型号和思考强度。');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** A detached frozen execution identity; no lookup of today's defaults. */
|
|
16
|
+
export function learningExecutionConfig(run) {
|
|
17
|
+
const value = run?.executionConfig;
|
|
18
|
+
if (!Number.isSafeInteger(run?.defaultsRevision) || run.defaultsRevision < 1 || !value ||
|
|
19
|
+
Object.keys(value).sort().join() !== [...EXECUTION].sort().join())
|
|
20
|
+
fail('learning_execution_config_missing', '旧学习没有可核实的执行配置,不能采用今天的设置重发。');
|
|
21
|
+
assertChoice(value);
|
|
22
|
+
if (!Number.isSafeInteger(value.maxTokens) || value.maxTokens < 1 ||
|
|
23
|
+
typeof value.preset !== 'string' || !value.preset ||
|
|
24
|
+
typeof value.cwd !== 'string' || !isAbsolute(value.cwd))
|
|
25
|
+
fail('learning_execution_config_missing', '学习执行配置不完整,不能自动恢复。');
|
|
26
|
+
return Object.freeze({ ...value });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** One independent default in the existing business store, not a main model or execution queue. */
|
|
30
|
+
export class LearningDefaults {
|
|
31
|
+
constructor({ business, deployment, assertSelection } = {}) {
|
|
32
|
+
if (!business?.storage) throw new TypeError('Learning defaults require business storage');
|
|
33
|
+
this.store = business.storage;
|
|
34
|
+
this.deployment = deployment ? Object.freeze({ ...choice(deployment), maxTokens: deployment.maxTokens,
|
|
35
|
+
preset: deployment.preset, cwd: deployment.cwd }) : null;
|
|
36
|
+
this.assertSelection = assertSelection;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
read() { return this.store.get(KIND, ID); }
|
|
40
|
+
|
|
41
|
+
validate(selected) {
|
|
42
|
+
assertChoice(selected);
|
|
43
|
+
const executionConfig = learningExecutionConfig({ defaultsRevision: 1,
|
|
44
|
+
executionConfig: { ...choice(selected), maxTokens: this.deployment?.maxTokens,
|
|
45
|
+
preset: this.deployment?.preset, cwd: this.deployment?.cwd } });
|
|
46
|
+
if (typeof this.assertSelection !== 'function')
|
|
47
|
+
fail('learning_configuration_unavailable', '学习设置尚未接入当前授权路线检查。');
|
|
48
|
+
const result = this.assertSelection(executionConfig);
|
|
49
|
+
if (result && typeof result.then === 'function') {
|
|
50
|
+
Promise.resolve(result).catch(() => {});
|
|
51
|
+
fail('learning_validator_async', '学习设置授权检查必须同步完成。');
|
|
52
|
+
}
|
|
53
|
+
if (result === false) fail('learning_configuration_unavailable', '当前路线不允许这项学习设置。');
|
|
54
|
+
return executionConfig;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
initialize() {
|
|
58
|
+
return this.store.transaction(() => {
|
|
59
|
+
const existing = this.read();
|
|
60
|
+
if (existing) return existing;
|
|
61
|
+
this.validate(this.deployment);
|
|
62
|
+
return this.store.save(KIND, { id: ID, ...choice(this.deployment),
|
|
63
|
+
provenance: { kind: 'explicit_learning_deployment', initialSelection: choice(this.deployment) } });
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
save(request) {
|
|
68
|
+
if (!request || Object.keys(request).sort().join() !== [...CHOICE, 'expectedRevision'].sort().join() ||
|
|
69
|
+
!Number.isSafeInteger(request.expectedRevision) || request.expectedRevision < 1)
|
|
70
|
+
fail('invalid_learning_defaults', '只接受模型三字段和明确的原版本;输出上限与工作目录属于部署配置。');
|
|
71
|
+
return this.store.transaction(() => {
|
|
72
|
+
const existing = this.read();
|
|
73
|
+
if (!existing) fail('learning_configuration_unavailable', '未明确配置学习,不能通过日常设置启用。');
|
|
74
|
+
if (existing.revision !== request.expectedRevision) fail('revision_conflict', '学习默认设置已变化,请刷新后保存。');
|
|
75
|
+
this.validate(request);
|
|
76
|
+
return this.store.save(KIND, { ...existing, ...choice(request),
|
|
77
|
+
provenance: { kind: 'validated_learning_default_edit', previousRevision: existing.revision,
|
|
78
|
+
initializedFrom: existing.provenance.initializedFrom ?? existing.provenance } },
|
|
79
|
+
{ expectedRevision: existing.revision });
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
snapshot() {
|
|
84
|
+
return this.store.transaction(() => {
|
|
85
|
+
const record = this.read();
|
|
86
|
+
if (!record) fail('learning_configuration_unavailable', '学习尚无明确的默认设置。');
|
|
87
|
+
return Object.freeze({ executionConfig: this.validate(record), defaultsRevision: record.revision });
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Compatibility until the runtime supplies its live authorized-route validator. */
|
|
93
|
+
export function deploymentSelectionValidator(deployment) {
|
|
94
|
+
return selected => {
|
|
95
|
+
if (!deployment || CHOICE.some(key => selected[key] !== deployment[key]))
|
|
96
|
+
fail('learning_configuration_unavailable', '当前组件只允许显式部署的学习选择;动态路线尚未接通。');
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { linkWork } from './goal-work.mjs';
|
|
2
|
+
// Business-only adaptation of Arcfold 3.0 service.mjs applyGoalChanges,
|
|
3
|
+
// saveGoalTransition and invalidateDependents. No jobs are created/cancelled.
|
|
4
|
+
import { ContractError, digest } from './learning-contracts.mjs';
|
|
5
|
+
import { pauseMethod } from './method-lifecycle.mjs';
|
|
6
|
+
|
|
7
|
+
const invalid = message => { throw new ContractError('invalid_output', message); };
|
|
8
|
+
|
|
9
|
+
export function saveGoalTransition(store, before, after, { commandId, pendingId, reason, sourceIds = [], actor = 'assistant_inference' }) {
|
|
10
|
+
const id = 'goal-audit-' + digest(commandId);
|
|
11
|
+
if (store.get('audit', id)) throw new ContractError('learning_conflict', '目标变更身份已经使用');
|
|
12
|
+
const saved = store.save('goals', after, { expectedRevision: before?.revision ?? null });
|
|
13
|
+
store.save('audit', { id, commandId, goalId: saved.id, actor, reason,
|
|
14
|
+
pendingId, sourceIds, before: before || null, after: saved });
|
|
15
|
+
return saved;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function assertAcyclic(goals) {
|
|
19
|
+
const byId = new Map(goals.map(goal => [goal.id, goal])), edges = new Map();
|
|
20
|
+
for (const goal of goals) {
|
|
21
|
+
if (goal.deleted || goal.withdrawn || goal.useProhibited) continue;
|
|
22
|
+
for (const relation of goal.relations || []) {
|
|
23
|
+
const target = byId.get(relation.targetId);
|
|
24
|
+
if (relation.type === 'part_of' && target && !target.deleted && !target.withdrawn &&
|
|
25
|
+
!target.useProhibited && target.definitionVersion === relation.targetDefinitionVersion)
|
|
26
|
+
edges.set(goal.id, [...(edges.get(goal.id) || []), target.id]);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const visiting = new Set(), done = new Set();
|
|
30
|
+
function visit(id) {
|
|
31
|
+
if (visiting.has(id)) invalid('目标组成关系不能成环');
|
|
32
|
+
if (done.has(id)) return;
|
|
33
|
+
visiting.add(id);
|
|
34
|
+
for (const next of edges.get(id) || []) visit(next);
|
|
35
|
+
visiting.delete(id); done.add(id);
|
|
36
|
+
}
|
|
37
|
+
for (const id of byId.keys()) visit(id);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** All rows and their definition/relationship versions share one section transaction. */
|
|
41
|
+
export function applyGoalChanges(store, { pendingId, material, rows, assertTarget, assertSources, understandingRefs }) {
|
|
42
|
+
const aliases = new Map(), ids = new Set(), prepared = [];
|
|
43
|
+
for (const [index, row] of rows.entries()) {
|
|
44
|
+
const old = row.operation === 'update' ? assertTarget('goals', row.targetId) : null;
|
|
45
|
+
const repair = old && material.existingGoals.find(goal => goal.id === old.id)?.repairOnly === true;
|
|
46
|
+
if (repair && (!old.useProhibited || old.sourceInvalidation?.kind !== 'source_dependency' ||
|
|
47
|
+
old.withdrawn || old.deleted || !row.sourceIds.some(id => material.inputEventIds.includes(id))))
|
|
48
|
+
invalid('重新澄清须有本次新用户原话,且只能修复程序标记的来源失效目标');
|
|
49
|
+
if (row.operation === 'create' && row.targetId !== '') invalid('新建目标不能指定旧编号');
|
|
50
|
+
if ((row.operation === 'update' && row.clientRef) ||
|
|
51
|
+
(row.clientRef && (!row.clientRef.startsWith('new-goal:') || aliases.has(row.clientRef))))
|
|
52
|
+
invalid('临时目标编号仅限同区新建且不可重复');
|
|
53
|
+
const id = old?.id || 'goal-' + digest([pendingId, index]).slice(0, 32);
|
|
54
|
+
if (ids.has(id)) invalid('同一区不能重复修改一个目标');
|
|
55
|
+
ids.add(id);
|
|
56
|
+
if (row.clientRef) aliases.set(row.clientRef, id);
|
|
57
|
+
const references = understandingRefs(row.understandingIds);
|
|
58
|
+
const dependencies = [...assertSources(row.sourceIds, true), ...references];
|
|
59
|
+
const scope = row.scope === 'global' ? {} : { conversationId: old?.conversationId || material.sessionId };
|
|
60
|
+
const definition = { title: row.title, outcome: row.outcome, scope, understandingRefs: references };
|
|
61
|
+
const previous = old && { title: old.title, outcome: old.outcome, scope: old.scope || {},
|
|
62
|
+
understandingRefs: (old.dependencies || []).filter(ref => ref.kind === 'understandings') };
|
|
63
|
+
const definitionVersion = old ? old.definitionVersion + Number(digest(definition) !== digest(previous)) : 1;
|
|
64
|
+
if (!Number.isSafeInteger(definitionVersion) || definitionVersion < 1) invalid('旧目标缺少有效定义版本');
|
|
65
|
+
if (!row.progress.content.trim() && (row.progress.content !== '' || row.progress.sourceIds.length))
|
|
66
|
+
invalid('空进展必须使用空文字和空来源');
|
|
67
|
+
const progressDependencies = row.progress.content ? assertSources(row.progress.sourceIds) : [];
|
|
68
|
+
const controlSources = row.sourceIds.map(id => store.get('events', id)).filter(source =>
|
|
69
|
+
material.inputEventIds.includes(source?.id) && source.message?.role === 'user' &&
|
|
70
|
+
source.message.source?.kind === 'user' && source.message.source.principal === undefined);
|
|
71
|
+
if (row.controlProposal !== 'none' && !controlSources.length)
|
|
72
|
+
invalid('追踪解释必须引用本轮真实owner原话,后台续轮不能改变追踪');
|
|
73
|
+
const controlBoundary = old?.userControl?.nativeBoundary;
|
|
74
|
+
const laterThanHumanControl = !old?.userControl || controlBoundary && controlSources.some(source => {
|
|
75
|
+
const ref = source.provenance?.sourceRef;
|
|
76
|
+
return ref?.type === 'agent/inbox/spliced' && ref.sessionId === controlBoundary.sessionId &&
|
|
77
|
+
ref.seq > controlBoundary.throughSeq;
|
|
78
|
+
});
|
|
79
|
+
const applyTracking = row.controlProposal !== 'none' && laterThanHumanControl;
|
|
80
|
+
|
|
81
|
+
const next = { ...old, id, conversationId: old?.conversationId || material.sessionId,
|
|
82
|
+
title: row.title, outcome: row.outcome, scope, definitionVersion,
|
|
83
|
+
understandingIds: row.understandingIds, sourceIds: row.sourceIds, dependencies,
|
|
84
|
+
status: applyTracking ? { pause: 'paused', resume: 'tracking', close: 'closed' }[row.controlProposal] : old?.status || 'tracking',
|
|
85
|
+
userControl: old?.userControl ?? null,
|
|
86
|
+
...(row.controlProposal !== 'none' ? { trackingInterpretation: { action: row.controlProposal,
|
|
87
|
+
sourceIds: controlSources.map(source => source.id), pendingId, claimType: 'inference',
|
|
88
|
+
applied: Boolean(applyTracking), reason: applyTracking ? 'current_owner_input' : 'prior_user_control_order_unproven' } } : {}),
|
|
89
|
+
progress: { content: row.progress.content, sourceIds: row.progress.sourceIds,
|
|
90
|
+
dependencies: progressDependencies, runId: pendingId },
|
|
91
|
+
relations: [], controlProposal: row.controlProposal === 'none' ? null : {
|
|
92
|
+
action: row.controlProposal, definitionVersion, sourceIds: row.sourceIds,
|
|
93
|
+
dependencies: assertSources(row.sourceIds), claimType: 'inference', runId: pendingId },
|
|
94
|
+
claimType: 'inference', contentAuthor: 'assistant', userEndorsement: 'not_established_by_this_record',
|
|
95
|
+
learnedFromRunId: old?.learnedFromRunId || pendingId, updatedFromRunId: pendingId };
|
|
96
|
+
if (repair) {
|
|
97
|
+
next.useProhibited = false;
|
|
98
|
+
next.sourceInvalidation = null;
|
|
99
|
+
next.withdrawalReason = null;
|
|
100
|
+
}
|
|
101
|
+
prepared.push({ old, next, row, index });
|
|
102
|
+
}
|
|
103
|
+
const replacements = new Map(prepared.map(item => [item.next.id, item.next]));
|
|
104
|
+
for (const item of prepared) {
|
|
105
|
+
const seen = new Set();
|
|
106
|
+
for (const relation of item.row.relations) {
|
|
107
|
+
const targetId = aliases.get(relation.targetId) || relation.targetId;
|
|
108
|
+
const target = replacements.get(targetId) || assertTarget('goals', targetId);
|
|
109
|
+
const key = relation.type + ':' + targetId;
|
|
110
|
+
if (!target || targetId === item.next.id || seen.has(key)) invalid('目标关系不能自连、重复或缺少两端');
|
|
111
|
+
seen.add(key);
|
|
112
|
+
// A full Learn update explicitly reproposes each relation. Bind both
|
|
113
|
+
// definitions; retaining an old stored edge never silently rebases it.
|
|
114
|
+
item.next.relations.push({ ...relation, targetId, sourceDefinitionVersion: item.next.definitionVersion,
|
|
115
|
+
targetDefinitionVersion: target.definitionVersion,
|
|
116
|
+
dependencies: assertSources(relation.sourceIds) });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
assertAcyclic([...store.listAll('goals').filter(goal => !replacements.has(goal.id)), ...replacements.values()]);
|
|
120
|
+
const result = prepared.map(({ old, next, index }) => saveGoalTransition(store, old, next, {
|
|
121
|
+
commandId: `${pendingId}:goals:${index}`, pendingId, reason: old ? 'learn_update' : 'learn_create',
|
|
122
|
+
sourceIds: next.sourceIds }).id);
|
|
123
|
+
const workIds = new Set();
|
|
124
|
+
for (const { next, row, index } of prepared) for (const selection of row.workLinks || []) {
|
|
125
|
+
if (workIds.has(selection.workId)) invalid('同一工作在本区只能联系一个明确目标');
|
|
126
|
+
workIds.add(selection.workId);
|
|
127
|
+
const shown = material.goalWorks?.find(work => work.id === selection.workId);
|
|
128
|
+
if (!shown || shown.sessionId !== material.sessionId) invalid('工作联系必须逐字选择本次交付的真实工作编号');
|
|
129
|
+
assertSources(selection.sourceIds);
|
|
130
|
+
linkWork(store, { work: store.get('goal_works', shown.id), goal: store.get('goals', next.id),
|
|
131
|
+
state: shown, reason: selection.reason, sourceIds: selection.sourceIds,
|
|
132
|
+
expectedRelationId: shown.relationId,
|
|
133
|
+
origin: { kind: 'learning', pendingId, index } });
|
|
134
|
+
}
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Invalidate dependent business knowledge only; execution remains host-owned. */
|
|
139
|
+
export function invalidateDependents(store, { ids, pendingId, reason }) {
|
|
140
|
+
const invalidIds = new Set(ids), changes = [];
|
|
141
|
+
const inventory = ['understandings', 'methods', 'opportunities', 'goals']
|
|
142
|
+
.flatMap(kind => store.listAll(kind).map(record => ({ kind, record })));
|
|
143
|
+
let advanced;
|
|
144
|
+
do {
|
|
145
|
+
advanced = false;
|
|
146
|
+
for (const item of inventory) {
|
|
147
|
+
const { kind, record } = item;
|
|
148
|
+
if (invalidIds.has(record.id) || (!(record.sourceIds || []).some(id => invalidIds.has(id)) &&
|
|
149
|
+
!(record.dependencies || []).some(ref => invalidIds.has(ref.id)))) continue;
|
|
150
|
+
invalidIds.add(record.id); advanced = true;
|
|
151
|
+
const next = kind === 'goals'
|
|
152
|
+
? { ...record, useProhibited: true, sourceInvalidation: { kind: 'source_dependency', reason }, withdrawalReason: reason }
|
|
153
|
+
: { ...record, status: kind === 'understandings' ? 'withdrawn' : kind === 'methods' ? 'paused' : 'dismissed',
|
|
154
|
+
withdrawalReason: reason, ...(kind === 'methods' ? { pauseReason: reason } : {}) };
|
|
155
|
+
item.record = kind === 'methods'
|
|
156
|
+
? pauseMethod(store, record, { reason, changes: { withdrawalReason: reason }, pendingId })
|
|
157
|
+
: store.save(kind, next, { expectedRevision: record.revision });
|
|
158
|
+
changes.push({ kind, id: record.id, revision: item.record.revision });
|
|
159
|
+
if (kind === 'goals') store.save('audit', {
|
|
160
|
+
id: 'goal-audit-' + digest([pendingId, reason, record.id, record.revision]),
|
|
161
|
+
goalId: record.id, pendingId, actor: 'program_source_guard', reason, before: record, after: item.record });
|
|
162
|
+
}
|
|
163
|
+
} while (advanced);
|
|
164
|
+
return changes;
|
|
165
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { LearningService } from './learning.mjs';
|
|
2
|
+
import { LearningRunner } from './learning-runner.mjs';
|
|
3
|
+
import { LearningDefaults, deploymentSelectionValidator } from './learning-defaults.mjs';
|
|
4
|
+
|
|
5
|
+
export const name = 'arcfold-learning';
|
|
6
|
+
export const inject = ['arcfoldBusiness', 'arcfoldBridge', 'agents', 'agentPresets', 'sessions', 'sessionPersistence', 'workingDirectory', 'llm'];
|
|
7
|
+
export async function apply(ctx, config = {}) {
|
|
8
|
+
const learning = new LearningService({ business: ctx.arcfoldBusiness, maxCharacters: config.maxCharacters ?? 180000 });
|
|
9
|
+
const deployment = { ...config, cwd: config.cwd ?? ctx.workingDirectory.defaultDirectory };
|
|
10
|
+
const defaults = new LearningDefaults({ business: ctx.arcfoldBusiness, deployment,
|
|
11
|
+
assertSelection: ctx.get('arcfoldRuntimeModels')?.assertLearningSelection ?? deploymentSelectionValidator(deployment) });
|
|
12
|
+
const runner = new LearningRunner({ business: ctx.arcfoldBusiness, learning, host: ctx, config, defaults, onSettled: pendingId => ctx.emit('arcfold/opportunities-committed', pendingId), logger: ctx.logger });
|
|
13
|
+
ctx.effect(() => () => runner.close(), 'stop auxiliary learning before its business storage');
|
|
14
|
+
ctx.on('llm/stream', (options, next) => runner.guardStream(options, next));
|
|
15
|
+
ctx.on('session/event', (session, event) => {
|
|
16
|
+
if (event.type !== 'turn/end' || !ctx.arcfoldBridge.isBound(session.id)) return;
|
|
17
|
+
// Bridge observation schedules reconciliation synchronously. Await only that
|
|
18
|
+
// durable business commit, never a main-turn hook or another main Agent.
|
|
19
|
+
void Promise.resolve().then(async () => {
|
|
20
|
+
await ctx.arcfoldBridge.pending.get(session.id);
|
|
21
|
+
if (!runner.closed) await runner.kick();
|
|
22
|
+
}).catch(error => ctx.logger.error?.(error));
|
|
23
|
+
});
|
|
24
|
+
ctx.provide('arcfoldLearningDefaults', defaults);
|
|
25
|
+
ctx.provide('arcfoldLearning', runner);
|
|
26
|
+
await runner.recover();
|
|
27
|
+
void runner.kick().catch(error => ctx.logger.error?.(error));
|
|
28
|
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { hash } from './context-material.mjs';
|
|
3
|
+
import { canonicalJson } from './store.mjs';
|
|
4
|
+
import { LearningDefaults, learningExecutionConfig, deploymentSelectionValidator } from './learning-defaults.mjs';
|
|
5
|
+
|
|
6
|
+
// DSH rc.1 keeps this required fact even when optional context is suppressed.
|
|
7
|
+
// Match workingDirectory.contextText + joinContextSections, never a free-form
|
|
8
|
+
// model instruction. Native projection may regenerate its message id.
|
|
9
|
+
export function directoryContext(cwd) {
|
|
10
|
+
const section = { name: 'working-directory:current', text: `Current working directory: ${JSON.stringify(cwd)}.` };
|
|
11
|
+
return { role: 'user', source: { kind: 'runtime-context', form: 'snapshot', sections: [section] },
|
|
12
|
+
content: [{ type: 'text', text: `Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\n${section.text}` }] };
|
|
13
|
+
}
|
|
14
|
+
export function contextMatches(message, expected) {
|
|
15
|
+
if (!message || typeof message.id !== 'string' || !message.id) return false;
|
|
16
|
+
const { id, ...body } = message;
|
|
17
|
+
return canonicalJson(body) === canonicalJson(expected);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Dispatches durable learning intents through DSH; it contains no model/tool loop. */
|
|
21
|
+
export class LearningRunner {
|
|
22
|
+
constructor({ business, learning, host, config, defaults, onSettled = () => {}, logger = console }) {
|
|
23
|
+
if (!config || !['preset', 'provider', 'model', 'reasoningEffort'].every(key => typeof config[key] === 'string' && config[key]))
|
|
24
|
+
throw new Error('Learning requires an explicit preset, provider, model and reasoningEffort');
|
|
25
|
+
if (!Number.isSafeInteger(config.maxTokens) || config.maxTokens < 1)
|
|
26
|
+
throw new Error('Learning requires an explicit positive maxTokens');
|
|
27
|
+
Object.assign(this, { business, learning, host, config: { ...config }, logger });
|
|
28
|
+
this.storage = business.storage;
|
|
29
|
+
this.defaults = defaults ?? new LearningDefaults({ business,
|
|
30
|
+
deployment: { ...config, cwd: config.cwd ?? host.workingDirectory?.defaultDirectory },
|
|
31
|
+
assertSelection: deploymentSelectionValidator(config) });
|
|
32
|
+
this.defaults.initialize();
|
|
33
|
+
this.onSettled = onSettled;
|
|
34
|
+
this.closed = false;
|
|
35
|
+
this.active = null;
|
|
36
|
+
this.handle = null;
|
|
37
|
+
this.rerunRequested = false;
|
|
38
|
+
this.closing = null;
|
|
39
|
+
this.creation = new AbortController();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
executionConfigFor(id) { return learningExecutionConfig(this.storage.get('learning_runs', id)); }
|
|
43
|
+
|
|
44
|
+
updateRun(id, changes) {
|
|
45
|
+
const run = this.storage.get('learning_runs', id);
|
|
46
|
+
if (!run) throw new Error('Missing learning dispatch identity');
|
|
47
|
+
if ('executionConfig' in changes || 'defaultsRevision' in changes)
|
|
48
|
+
throw new Error('A started learning execution configuration cannot be replaced');
|
|
49
|
+
return this.storage.save('learning_runs', { ...run, ...changes }, { expectedRevision: run.revision });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
attention(id, error) {
|
|
53
|
+
const details = { code: error?.code || 'learning_host_failure', message: String(error?.message || error) };
|
|
54
|
+
this.storage.transaction(() => {
|
|
55
|
+
const run = this.storage.get('learning_runs', id);
|
|
56
|
+
if (run && run.state !== 'settled') this.updateRun(id, {
|
|
57
|
+
state: run.state === 'prepared' ? 'failed' : 'unknown', error: details,
|
|
58
|
+
});
|
|
59
|
+
const pending = this.storage.get('learning_pending', id);
|
|
60
|
+
if (pending?.status === 'pending') this.storage.save('learning_pending', {
|
|
61
|
+
...pending, status: 'needs_attention', error: details,
|
|
62
|
+
effect: 'the delivered answer is retained; no automatic model retry',
|
|
63
|
+
}, { expectedRevision: pending.revision });
|
|
64
|
+
});
|
|
65
|
+
this.logger.warn?.(`Arcfold learning ${id}: ${details.message}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
kick() {
|
|
69
|
+
if (this.closed) return this.active || Promise.resolve();
|
|
70
|
+
if (this.active) { this.rerunRequested = true; return this.active; }
|
|
71
|
+
this.rerunRequested = false;
|
|
72
|
+
// Leave synchronous event publication. One helper at a time, independently
|
|
73
|
+
// of all main Agents; a new human input never awaits this promise.
|
|
74
|
+
const task = Promise.resolve().then(async () => {
|
|
75
|
+
while (!this.closed) {
|
|
76
|
+
const pending = this.storage.list('learning_pending', { where: { status: 'pending' }, limit: 1 })[0];
|
|
77
|
+
if (!pending) return;
|
|
78
|
+
try { await this.runOne(pending.id); }
|
|
79
|
+
catch (error) { this.attention(pending.id, error); }
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
this.active = task;
|
|
83
|
+
void task.finally(() => {
|
|
84
|
+
if (this.active === task) this.active = null;
|
|
85
|
+
if (this.rerunRequested && !this.closed) void this.kick().catch(error => this.logger.error?.(error));
|
|
86
|
+
}).catch(error => this.logger.error?.(error));
|
|
87
|
+
return task;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async readSession(sessionId, ownedHandle = null) {
|
|
91
|
+
const live = this.host.sessions.get(sessionId);
|
|
92
|
+
if (live && live !== ownedHandle?.agent.session) throw new Error('Recovery refused: the learning session already has a live owner');
|
|
93
|
+
let handle;
|
|
94
|
+
try { handle = await this.host.sessionPersistence.open(sessionId, 'read'); }
|
|
95
|
+
catch (error) {
|
|
96
|
+
if (error?.name === 'SessionPersistenceNotFoundError' && error.sessionId === sessionId) return null;
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
try { return (await handle.read()).events; }
|
|
100
|
+
finally { await handle.close(); }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
guardStream(options, next) {
|
|
104
|
+
const runs = typeof options.sessionId === 'string'
|
|
105
|
+
? this.storage.listAll('learning_runs', { where: { sessionId: options.sessionId } }) : [];
|
|
106
|
+
if (!runs.length) return next();
|
|
107
|
+
const runner = this;
|
|
108
|
+
return (async function* () {
|
|
109
|
+
options.signal?.throwIfAborted();
|
|
110
|
+
const run = runner.storage.get('learning_runs', runs[0].id);
|
|
111
|
+
const execution = runner.executionConfigFor(run.id);
|
|
112
|
+
const input = runner.learning.prepare(run.id);
|
|
113
|
+
const originals = options.messages.filter(message => message.source?.kind === 'arcfold/learning');
|
|
114
|
+
const contexts = options.messages.filter(message => message.source?.kind === 'runtime-context');
|
|
115
|
+
const systems = options.messages.filter(message => message.role === 'system')
|
|
116
|
+
.flatMap(message => message.content || []).filter(block => block.type === 'text').map(block => block.text).join('\n');
|
|
117
|
+
const unexpected = options.messages.some(message => message.role !== 'system' &&
|
|
118
|
+
!['arcfold/learning', 'runtime-context'].includes(message.source?.kind));
|
|
119
|
+
const session = runner.host.sessions.get(options.sessionId);
|
|
120
|
+
if (runner.closed || runs.length !== 1 || run.state !== 'submitted' || run.dispatchCount !== 0 ||
|
|
121
|
+
options.purpose !== undefined || options.provider !== execution.provider || options.model !== execution.model ||
|
|
122
|
+
options.reasoningEffort !== execution.reasoningEffort || options.maxTokens !== execution.maxTokens ||
|
|
123
|
+
options.tools?.length || options.toolHistory?.tools?.length || options.toolHistory?.updates?.length ||
|
|
124
|
+
unexpected || options.system !== undefined ||
|
|
125
|
+
options.messages.length !== 3 || originals.length !== 1 || hash(originals[0]) !== hash(run.message) || systems !== input.prompt ||
|
|
126
|
+
!session || run.hostCwd !== execution.cwd || runner.host.workingDirectory.get(session) !== run.hostCwd || contexts.length !== 1 ||
|
|
127
|
+
!contextMatches(contexts[0], run.hostContext))
|
|
128
|
+
throw new Error('Learning request differs from its single-call, no-tools, frozen-input contract');
|
|
129
|
+
// This is a dispatch intent, NOT proof that a provider received or billed
|
|
130
|
+
// it. Persist before entering downstream code; uncertainty never retries.
|
|
131
|
+
runner.updateRun(run.id, { dispatchCount: 1, requestHash: hash({ messages: options.messages, tools: options.tools || [] }),
|
|
132
|
+
runtimeContextMessage: contexts[0],
|
|
133
|
+
dispatchEvidence: 'validated-before-downstream-stream; provider outcome requires native events' });
|
|
134
|
+
yield* next();
|
|
135
|
+
})();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async setup(agentCtx, input, execution) {
|
|
139
|
+
await this.host.agentPresets.mount(agentCtx, execution.preset);
|
|
140
|
+
agentCtx.tools.restrict({ allow: [] });
|
|
141
|
+
// Only this no-tools helper has a complete frozen prompt. Main Arcfold
|
|
142
|
+
// Agents retain DSH's native persona/tool instructions and runtime context.
|
|
143
|
+
agentCtx.systemPrompt.suppressRuntimeContext();
|
|
144
|
+
agentCtx.systemPrompt.section({ name: 'arcfold:learning', order: 50, interpolate: false, complete: true, text: input.prompt });
|
|
145
|
+
agentCtx.on('agent/pre-step', async (payload, next) => {
|
|
146
|
+
const decision = await next();
|
|
147
|
+
if (decision.kind === 'reject') return decision;
|
|
148
|
+
// DSH prepares once more with no messages to finish a normal turn. That
|
|
149
|
+
// empty completion check is not another model call and must remain valid.
|
|
150
|
+
if (payload.step > 1 && decision.messages.length) throw new Error('Learning accepts exactly one input and one model request');
|
|
151
|
+
this.learning.prepare(input.id);
|
|
152
|
+
if (payload.step === 1) {
|
|
153
|
+
const cwd = this.host.workingDirectory.get(payload.agent.session);
|
|
154
|
+
const contexts = decision.messages.filter(message => message.source?.kind === 'runtime-context');
|
|
155
|
+
const expected = directoryContext(cwd);
|
|
156
|
+
if (cwd !== execution.cwd || contexts.length !== 1 || !contextMatches(contexts[0], expected))
|
|
157
|
+
throw new Error('Learning requires the exact native working-directory context and no other runtime facts');
|
|
158
|
+
this.updateRun(input.id, { hostCwd: cwd, hostContext: expected });
|
|
159
|
+
}
|
|
160
|
+
return decision;
|
|
161
|
+
});
|
|
162
|
+
return { commit: () => {
|
|
163
|
+
if (this.closed) throw new Error('Learning plugin is stopping');
|
|
164
|
+
this.learning.prepare(input.id);
|
|
165
|
+
} };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async runOne(id) {
|
|
169
|
+
let run = this.storage.get('learning_runs', id);
|
|
170
|
+
let existingEvents = null;
|
|
171
|
+
if (run) {
|
|
172
|
+
// Inspect before resume: resuming a DSH Agent can start its pending inbox.
|
|
173
|
+
existingEvents = await this.readSession(run.sessionId);
|
|
174
|
+
if (run.state !== 'prepared') {
|
|
175
|
+
if (['submitted', 'settled', 'unknown'].includes(run.state) && existingEvents?.some(event => event.type === 'turn/end')) {
|
|
176
|
+
if (run.state === 'unknown') this.updateRun(id, { state: 'submitted',
|
|
177
|
+
recoveredFrom: 'unknown; durable-native-log-found-without-model-dispatch' });
|
|
178
|
+
const result = this.learning.commit(id, { sessionId: run.sessionId, events: existingEvents });
|
|
179
|
+
this.updateRun(id, { state: 'settled', resultStatus: result.status, recoveredFrom: 'durable-native-log' });
|
|
180
|
+
this.onSettled(id);
|
|
181
|
+
return result;
|
|
182
|
+
}
|
|
183
|
+
throw new Error('Prior learning submission has no complete durable result; no request was repeated');
|
|
184
|
+
}
|
|
185
|
+
if (existingEvents?.some(event => ['turn/start', 'user/message', 'agent/inbox/spliced', 'assistant/request'].includes(event.type)))
|
|
186
|
+
throw new Error('Prepared learning identity already contains execution evidence; automatic resume refused');
|
|
187
|
+
}
|
|
188
|
+
// A legacy prepared row is never filled from today's configuration.
|
|
189
|
+
if (run) this.executionConfigFor(id);
|
|
190
|
+
let input;
|
|
191
|
+
this.storage.transaction(() => {
|
|
192
|
+
input = this.learning.prepare(id);
|
|
193
|
+
if (run) return;
|
|
194
|
+
const snapshot = this.defaults.snapshot();
|
|
195
|
+
const message = { id: `arcfold-learning-${randomUUID()}`, role: 'user',
|
|
196
|
+
source: { kind: 'arcfold/learning', pendingId: id, deliveryId: input.deliveryId },
|
|
197
|
+
content: [{ type: 'text', text: input.inputText }] };
|
|
198
|
+
run = this.storage.save('learning_runs', { id, pendingId: id, deliveryId: input.deliveryId,
|
|
199
|
+
sessionId: `session-${randomUUID()}`, messageId: message.id, message,
|
|
200
|
+
inputHash: input.inputHash, state: 'prepared', dispatchCount: 0, ...snapshot,
|
|
201
|
+
authority: 'auxiliary-learning; does-not-act-as-human-or-authorize-tools' });
|
|
202
|
+
});
|
|
203
|
+
if (this.closed) return;
|
|
204
|
+
const execution = this.executionConfigFor(id);
|
|
205
|
+
const agentOptions = { provider: execution.provider, model: execution.model, maxTokens: execution.maxTokens,
|
|
206
|
+
reasoningEffort: execution.reasoningEffort };
|
|
207
|
+
const setup = agentCtx => this.setup(agentCtx, input, execution);
|
|
208
|
+
const handle = existingEvents
|
|
209
|
+
? await this.host.agents.resume({ resumeSessionId: run.sessionId, agentOptions, setup, signal: this.creation.signal })
|
|
210
|
+
: await this.host.agents.create({ sessionId: run.sessionId, agentOptions, setup,
|
|
211
|
+
signal: this.creation.signal,
|
|
212
|
+
meta: { agentPreset: execution.preset, cwd: execution.cwd } });
|
|
213
|
+
this.handle = handle;
|
|
214
|
+
try {
|
|
215
|
+
if (this.closed) throw new Error('Learning plugin stopped before submission');
|
|
216
|
+
if (await this.host.sessions.flush(handle.agent.session) !== true)
|
|
217
|
+
throw new Error('Learning requires a durable native session writer');
|
|
218
|
+
this.learning.prepare(id);
|
|
219
|
+
this.updateRun(id, { state: 'submitted', submissionEvidence: 'intent persisted before followup; not proof of provider receipt' });
|
|
220
|
+
handle.agent.followup(run.message);
|
|
221
|
+
await handle.agent.whenIdle();
|
|
222
|
+
const expected = handle.agent.session.snapshotEvents();
|
|
223
|
+
let events;
|
|
224
|
+
try {
|
|
225
|
+
if (await this.host.sessions.flush(handle.agent.session) !== true)
|
|
226
|
+
throw new Error('Learning output was not confirmed durable');
|
|
227
|
+
events = await this.readSession(run.sessionId, handle);
|
|
228
|
+
} catch (error) {
|
|
229
|
+
if (!this.closed) throw error;
|
|
230
|
+
// Cordis unloads sibling effects in parallel, including the host's
|
|
231
|
+
// handle owner. Wait for that same disposer and then read durable data.
|
|
232
|
+
await handle.dispose();
|
|
233
|
+
events = await this.readSession(run.sessionId);
|
|
234
|
+
}
|
|
235
|
+
if (!events || events.length < expected.length || hash(events.slice(0, expected.length)) !== hash(expected))
|
|
236
|
+
throw new Error('Durable learning readback differs from the completed native prefix');
|
|
237
|
+
const result = this.learning.commit(id, { sessionId: run.sessionId, events });
|
|
238
|
+
this.updateRun(id, { state: 'settled', resultStatus: result.status });
|
|
239
|
+
this.onSettled(id);
|
|
240
|
+
return result;
|
|
241
|
+
} finally {
|
|
242
|
+
await handle.dispose();
|
|
243
|
+
if (this.handle === handle) this.handle = null;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
close() {
|
|
248
|
+
if (!this.closing) {
|
|
249
|
+
this.closed = true;
|
|
250
|
+
this.creation.abort(new Error('Learning plugin is stopping'));
|
|
251
|
+
// DSH owns cancellation, native session persistence and tool cleanup.
|
|
252
|
+
this.closing = (async () => {
|
|
253
|
+
this.handle?.agent.cancel({ kind: 'disposed' }, { keepInbox: true });
|
|
254
|
+
await this.active;
|
|
255
|
+
})();
|
|
256
|
+
}
|
|
257
|
+
return this.closing;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async recover() {
|
|
261
|
+
// Read-only recovery of already-submitted work; never creates/resumes an
|
|
262
|
+
// Agent. Complete durable output can be committed even after a prior stop.
|
|
263
|
+
for (const run of this.storage.listAll('learning_runs')) {
|
|
264
|
+
if (!['submitted', 'unknown'].includes(run.state)) continue;
|
|
265
|
+
try {
|
|
266
|
+
const events = await this.readSession(run.sessionId);
|
|
267
|
+
if (!events?.some(event => event.type === 'turn/end')) throw new Error('Learning outcome remains unknown; request was not repeated');
|
|
268
|
+
if (run.state === 'unknown') this.updateRun(run.id, { state: 'submitted', recoveredFrom: 'durable-native-log; no-model-retry' });
|
|
269
|
+
const result = this.learning.commit(run.id, { sessionId: run.sessionId, events });
|
|
270
|
+
this.updateRun(run.id, { state: 'settled', resultStatus: result.status, recoveredFrom: 'durable-native-log' });
|
|
271
|
+
this.onSettled(run.id);
|
|
272
|
+
} catch (error) { this.attention(run.id, error); }
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|