@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
package/src/goals.mjs ADDED
@@ -0,0 +1,578 @@
1
+ import { workViews, assertWork, workId as nativeWorkId, goalContentHash, frozenGoalCurrent } from './goal-work.mjs';
2
+ // Business goals and their relation to native work. This service never runs work.
3
+ import { DomainError, canonicalJson, identity } from './store.mjs';
4
+ import { hash, sourceText, sourceAvailable, dependenciesCurrent, evidenceProvenance } from './evidence.mjs';
5
+ import { saveGoalTransition } from './learning-goals.mjs';
6
+ import { goalContextMessage } from './goal-context.mjs';
7
+ import { browseScope, browseContext } from './browse-scope.mjs';
8
+
9
+ const fail = (code, message) => { throw new DomainError(code, message); };
10
+ const digest = value => hash(JSON.parse(canonicalJson(value)));
11
+ const positive = value => Number.isSafeInteger(value) && value > 0;
12
+ const nonnegative = value => Number.isSafeInteger(value) && value >= 0;
13
+ const inScope = (row, sessionId) => !row.scope?.conversationId || row.scope.conversationId === sessionId;
14
+ const atSequence = (row, cutoff) => row && nonnegative(row.createdSequence) && nonnegative(row.updatedSequence) &&
15
+ row.createdSequence <= cutoff && row.updatedSequence <= cutoff;
16
+ const states = new Set(['tracking', 'paused', 'closed']);
17
+ const copy = value => JSON.parse(JSON.stringify(value));
18
+ const evidenceView = bundle => ({ ...copy(bundle), sources: bundle.sources.map(source => ({
19
+ ...copy(source), provenance: evidenceProvenance(source.provenance) })) });
20
+
21
+ export const goalRepairAllowed = goal => Boolean(goal && goal.useProhibited === true &&
22
+ goal.sourceInvalidation?.kind === 'source_dependency' && !goal.withdrawn && !goal.deleted);
23
+ export { goalContentHash };
24
+
25
+ function position({ sessionId, turn, startSeq }) {
26
+ identity(sessionId, 'Native session id');
27
+ if (!nonnegative(turn) || !nonnegative(startSeq)) fail('invalid_native_turn', '需要真实宿主轮次和开始序号。');
28
+ }
29
+ function paging(offset, limit) {
30
+ if (!nonnegative(offset) || !positive(limit) || limit > 100) fail('invalid_pagination', '目标分页参数不合法。');
31
+ }
32
+ function metadata(goal) {
33
+ return { id: goal.id, revision: goal.revision, definitionVersion: goal.definitionVersion,
34
+ status: goal.status, repairOnly: true, reason: goal.sourceInvalidation?.reason || 'source_dependency' };
35
+ }
36
+
37
+ /** A paused/closed goal root remains discussable; its dependencies receive no exception. */
38
+ export function goalDefinitionCurrent(store, goal, cutoff = store.currentSequence()) {
39
+ return Boolean(goal && states.has(goal.status) && positive(goal.definitionVersion) &&
40
+ dependenciesCurrent(store, { ...goal, status: 'active' }, cutoff));
41
+ }
42
+
43
+ function sourcesFor(store, record) {
44
+ const sources = new Map(), visited = new Set();
45
+ function visit(row) {
46
+ if (!row || visited.has(row.id)) return;
47
+ visited.add(row.id);
48
+ for (const id of row.sourceIds || []) sources.set(id, store.get('events', id));
49
+ for (const ref of row.dependencies || []) {
50
+ const kind = ref.kind || ['events', 'understandings', 'methods'].find(k => store.get(k, ref.id));
51
+ if (kind === 'events') sources.set(ref.id, store.get(kind, ref.id));
52
+ else visit(store.get(kind, ref.id));
53
+ }
54
+ }
55
+ visit(record);
56
+ return [...sources.values()].filter(Boolean).map(source => ({ id: source.id, revision: source.revision,
57
+ role: source.role, content: sourceText(source), occurredAt: source.occurredAt ?? null,
58
+ provenance: evidenceProvenance(source.provenance) }));
59
+ }
60
+ function partCurrent(store, goal, part, cutoff) {
61
+ if (!part) return false;
62
+ if (part.content === '' && part.sourceIds?.length === 0) return true;
63
+ return dependenciesCurrent(store, { ...part, id: `goal-part:${goal.id}`, status: 'active',
64
+ createdSequence: goal.createdSequence, updatedSequence: goal.updatedSequence }, cutoff);
65
+ }
66
+
67
+ // Original knowledge dependencies are not execution state and never enter the
68
+ // model bundle twice. Keep their exact identities for later historical display.
69
+ function knowledgeReferences(store, entry) {
70
+ const found = new Map();
71
+ function visit(row) {
72
+ for (const ref of row?.dependencies || []) {
73
+ const kind = ref.kind || ['events', 'understandings', 'methods'].find(k => store.get(k, ref.id));
74
+ if (kind === 'events') continue;
75
+ const key = kind + ':' + ref.id;
76
+ if (found.has(key)) continue;
77
+ found.set(key, { kind, id: ref.id, revision: ref.revision });
78
+ visit(store.get(kind, ref.id));
79
+ }
80
+ }
81
+ for (const shown of [entry.goal, ...entry.relatedGoals]) {
82
+ if (shown.repairOnly) continue;
83
+ const goal = store.get('goals', shown.id);
84
+ visit(goal);
85
+ if (shown.progress) visit(shown.progress);
86
+ for (const relation of shown.relations || []) if (relation.state !== 'unavailable') visit(relation);
87
+ }
88
+ return [...found.values()];
89
+ }
90
+
91
+ export class GoalService {
92
+ constructor({ business }) {
93
+ if (!business?.storage) fail('invalid_goal_config', '需要业务存储。');
94
+ this.store = business.storage;
95
+ }
96
+
97
+ reconcile() {
98
+ return this.store.transaction(() => {
99
+ const changes = [];
100
+ for (const goal of this.store.listAll('goals')) {
101
+ if (goal.deleted || goal.withdrawn || goal.useProhibited || !states.has(goal.status) ||
102
+ goalDefinitionCurrent(this.store, goal)) continue;
103
+ changes.push(saveGoalTransition(this.store, goal, { ...goal, useProhibited: true,
104
+ sourceInvalidation: { kind: 'source_dependency', reason: 'source_changed' } }, {
105
+ commandId: `goal-source:${goal.id}:${goal.revision}`, pendingId: null,
106
+ actor: 'program_source_guard', reason: 'source_changed' }));
107
+ }
108
+ return changes;
109
+ });
110
+ }
111
+
112
+ view(goal, cutoff = this.store.currentSequence()) {
113
+ if (!goalDefinitionCurrent(this.store, goal, cutoff)) return null;
114
+ const sources = new Map(sourcesFor(this.store, goal).map(source => [source.id, source]));
115
+ const coverage = { progressUnavailable: 0, relationsUnavailable: 0, relationsNeedsReview: 0 };
116
+ const progress = partCurrent(this.store, goal, goal.progress, cutoff) ? copy(goal.progress) : null;
117
+ if (goal.progress && !progress) coverage.progressUnavailable++;
118
+ if (progress) for (const source of sourcesFor(this.store, goal.progress)) sources.set(source.id, source);
119
+ const relations = (goal.relations || []).map(relation => {
120
+ const target = this.store.get('goals', relation.targetId);
121
+ const state = !partCurrent(this.store, goal, relation, cutoff) ? 'unavailable'
122
+ : !goalDefinitionCurrent(this.store, target, cutoff) ? 'unavailable'
123
+ : (relation.sourceDefinitionVersion ?? goal.definitionVersion) !== goal.definitionVersion ||
124
+ relation.targetDefinitionVersion !== target.definitionVersion ? 'needs_review' : 'current';
125
+ if (state === 'unavailable') {
126
+ coverage.relationsUnavailable++;
127
+ return { type: relation.type, targetId: relation.targetId, state };
128
+ }
129
+ if (state === 'needs_review') coverage.relationsNeedsReview++;
130
+ for (const source of sourcesFor(this.store, relation)) sources.set(source.id, source);
131
+ return { ...copy(relation), state, targetStatus: target.status };
132
+ });
133
+ return { goal: { id: goal.id, revision: goal.revision, definitionVersion: goal.definitionVersion,
134
+ title: goal.title, outcome: goal.outcome, scope: goal.scope || {}, status: goal.status,
135
+ userControl: goal.userControl ?? null, sourceIds: [...goal.sourceIds],
136
+ claimType: 'inference', contentAuthor: 'assistant', userEndorsement: 'not_established_by_this_record',
137
+ progress, relations }, sources: [...sources.values()], coverage };
138
+ }
139
+
140
+ discussions({ sessionId, inputMessageIds = [], cutoffSequence = this.store.currentSequence() }) {
141
+ const ids = new Set(inputMessageIds);
142
+ const originals = this.store.listAll('events', { where: { sessionId } }).filter(source =>
143
+ // Official owner prompts omit principal; other callers are explicitly
144
+ // tagged by SessionController.prompt and cannot redeem a browser choice.
145
+ source.role === 'user' && source.message?.source?.kind === 'user' &&
146
+ source.message.source.principal === undefined && ids.has(source.message.id) &&
147
+ dependenciesCurrent(this.store, { id: 'discussion-source', status: 'active', sourceIds: [source.id],
148
+ dependencies: [{ kind: 'events', id: source.id, revision: source.revision }],
149
+ createdSequence: source.createdSequence, updatedSequence: source.updatedSequence }, cutoffSequence));
150
+ return this.store.listAll('goal_discussions', { where: { sessionId } }).flatMap(binding => {
151
+ if (binding.createdSequence > cutoffSequence) return [];
152
+ if (digest(binding.request) !== binding.requestHash) fail('goal_discussion_changed', '用户讨论绑定已被改写。');
153
+ const matches = originals.filter(source => source.message.source.rpcId === binding.requestId &&
154
+ digest(source.message.content) === binding.contentHash);
155
+ if (matches.length !== 1) return [];
156
+ const goal = this.store.get('goals', binding.goalId);
157
+ if (!goal || goal.revision !== binding.goalRevision || goal.definitionVersion !== binding.definitionVersion ||
158
+ !inScope(goal, sessionId)) fail('goal_discussion_changed', '已选目标已变化,请重新查看并明确选回。');
159
+ return [{ ...binding, sourceId: matches[0].id, messageId: matches[0].message.id }];
160
+ });
161
+ }
162
+
163
+ catalog({ sessionId, cutoffSequence = this.store.currentSequence(), inputMessageIds = [] }) {
164
+ identity(sessionId, 'Native session id');
165
+ if (!nonnegative(cutoffSequence)) fail('invalid_cutoff', '目标目录需要有效截止。');
166
+ const explicit = new Map(this.discussions({ sessionId, inputMessageIds, cutoffSequence }).map(row => [row.goalId, row]));
167
+ return this.store.listAll('goals').flatMap(goal => {
168
+ if (!inScope(goal, sessionId) || !atSequence(goal, cutoffSequence)) return [];
169
+ const discussion = explicit.get(goal.id);
170
+ if (discussion?.mode === 'repair' && goalRepairAllowed(goal)) return [{ ...metadata(goal),
171
+ contentHash: goalContentHash(goal), sourceIds: [discussion.sourceId], explicitDiscussion: true,
172
+ discussionId: discussion.id }];
173
+ if (!goalDefinitionCurrent(this.store, goal, cutoffSequence) ||
174
+ goal.status !== 'tracking' && discussion?.mode !== 'discuss') return [];
175
+ return [{ id: goal.id, revision: goal.revision, definitionVersion: goal.definitionVersion,
176
+ contentHash: goalContentHash(goal), title: goal.title, outcome: goal.outcome, status: goal.status,
177
+ sourceIds: [...goal.sourceIds], explicitDiscussion: Boolean(discussion), repairOnly: false,
178
+ ...(discussion ? { discussionId: discussion.id } : {}) }];
179
+ });
180
+ }
181
+
182
+ fullEntry(goal, shown, selection, snapshot, maxNodes = 6) {
183
+ if (shown.repairOnly) {
184
+ const binding = this.discussions({ sessionId: snapshot.sessionId,
185
+ inputMessageIds: snapshot.material.inputMessageIds, cutoffSequence: snapshot.material.cutoffSequence })
186
+ .find(row => row.id === shown.discussionId && row.mode === 'repair');
187
+ if (!binding || !goalRepairAllowed(goal)) fail('goal_repair_not_allowed', '修复没有匹配本轮真实用户原话。');
188
+ const source = this.store.get('events', binding.sourceId);
189
+ return { goal: metadata(goal), relatedGoals: [], sources: [{ id: source.id, revision: source.revision,
190
+ role: source.role, content: sourceText(source), occurredAt: source.occurredAt, provenance: evidenceProvenance(source.provenance) }],
191
+ coverage: { relatedOmitted: 0 }, selection: copy(selection), discussionId: binding.id };
192
+ }
193
+ const cutoff = snapshot.material.cutoffSequence, root = this.view(goal, cutoff);
194
+ if (!root) fail('goal_unavailable', '目标定义或其原件已失效。');
195
+ const sources = new Map(root.sources.map(source => [source.id, source])), seen = new Set([goal.id]);
196
+ const relatedGoals = [], queue = [...root.goal.relations], coverage = { ...root.coverage, relatedOmitted: 0 };
197
+ for (const relation of queue) {
198
+ if (relation.state !== 'current' || seen.has(relation.targetId)) continue;
199
+ seen.add(relation.targetId);
200
+ const target = this.store.get('goals', relation.targetId);
201
+ if (target.status !== 'tracking' || !inScope(target, snapshot.sessionId) || relatedGoals.length + 1 >= maxNodes) {
202
+ coverage.relatedOmitted++; continue;
203
+ }
204
+ const projected = this.view(target, cutoff);
205
+ if (!projected) { coverage.relatedOmitted++; continue; }
206
+ relatedGoals.push(projected.goal);
207
+ for (const source of projected.sources) sources.set(source.id, source);
208
+ queue.push(...projected.goal.relations);
209
+ }
210
+ return { goal: root.goal, relatedGoals, sources: [...sources.values()], coverage, selection: copy(selection) };
211
+ }
212
+
213
+ link({ sessionId, turn, startSeq, callId, contextSnapshotId, contextMessageId, selection, maxCharacters = 24000 }) {
214
+ position({ sessionId, turn, startSeq }); identity(callId, 'Native goal tool call');
215
+ identity(contextSnapshotId, 'Goal context snapshot'); identity(contextMessageId, 'Goal context message');
216
+ if (!selection || typeof selection !== 'object' || Array.isArray(selection) ||
217
+ Object.keys(selection).some(key => !['id', 'revision', 'definitionVersion', 'contentHash', 'reason', 'sourceIds'].includes(key)))
218
+ fail('invalid_goal_selection', '关联仅接受目标版本、贡献解释和来源,不接受确认或权限字段。');
219
+ if (!positive(maxCharacters) || maxCharacters > 180000) fail('goal_capacity', '目标完整材料容量不合法。');
220
+ this.reconcile();
221
+ return this.store.transaction(() => {
222
+ const snapshot = this.store.get('context_snapshots', contextSnapshotId);
223
+ const shown = snapshot?.material?.goals?.find(row => row.id === selection?.id);
224
+ const goal = shown && this.store.get('goals', shown.id);
225
+ if (this.store.get('host_bindings', sessionId)?.role !== 'main' || !snapshot ||
226
+ snapshot.sessionId !== sessionId || snapshot.turn !== turn || snapshot.contextMessageId !== contextMessageId || !goal)
227
+ fail('goal_context_mismatch', '目标选择没有对应本轮主会话已显示目录。');
228
+ if (goal.revision !== selection.revision || shown.revision !== selection.revision ||
229
+ goal.definitionVersion !== selection.definitionVersion || shown.definitionVersion !== selection.definitionVersion ||
230
+ goalContentHash(goal) !== selection.contentHash || shown.contentHash !== selection.contentHash)
231
+ fail('goal_selection_changed', '目标未显示或所见版本已经改变。');
232
+ if (typeof selection.reason !== 'string' || !selection.reason.trim() || selection.reason.length > 2000 ||
233
+ !Array.isArray(selection.sourceIds) || !selection.sourceIds.length ||
234
+ new Set(selection.sourceIds).size !== selection.sourceIds.length ||
235
+ selection.sourceIds.some(id => !shown.sourceIds.includes(id)))
236
+ fail('invalid_goal_selection', '关联须保留本次贡献解释和已显示的来源。');
237
+ const currentShown = this.catalog({ sessionId, cutoffSequence: snapshot.material.cutoffSequence,
238
+ inputMessageIds: snapshot.material.inputMessageIds }).find(row => row.id === goal.id);
239
+ if (!currentShown || canonicalJson(currentShown) !== canonicalJson(shown))
240
+ fail('goal_unavailable', '目标资格、来源或用户讨论绑定已变化。');
241
+ const id = 'goal-link-' + digest([sessionId, startSeq]);
242
+ const prior = this.store.get('goal_links', id);
243
+ if (prior) {
244
+ if (prior.turn !== turn || prior.contextSnapshotId !== contextSnapshotId || prior.contextMessageId !== contextMessageId ||
245
+ canonicalJson(prior.selection) !== canonicalJson(selection))
246
+ fail('goal_link_conflict', '本轮已有不同的目标关联;不能覆盖原选择。');
247
+ this.assertCurrent({ linkId: id, sessionId, turn, startSeq });
248
+ return { receipt: this.receipt(prior), bundle: this.currentBundle({ sessionId, turn, startSeq }) };
249
+ }
250
+ const entry = { linkId: id, ...this.fullEntry(goal, shown, selection, snapshot) };
251
+ const bundle = this.emptyBundle({ sessionId, turn, startSeq }); bundle.links.push(entry);
252
+ if (goalContextMessage(bundle).content[0].text.length > maxCharacters)
253
+ fail('goal_capacity', '目标完整定义、关系、原件及说明超过容量;本次未保存关联。');
254
+ const core = { id, sessionId, turn, startSeq, callId, goalId: goal.id, goalRevision: goal.revision,
255
+ definitionVersion: goal.definitionVersion, contentHash: goalContentHash(goal), selection: copy(selection),
256
+ contextSnapshotId, contextMessageId, cutoffSequence: snapshot.material.cutoffSequence, bundle: entry,
257
+ dependencyRefs: knowledgeReferences(this.store, entry),
258
+ authority: 'assistant-inference-about-current-contribution; not-user-endorsement-or-execution-permission' };
259
+ const link = this.store.save('goal_links', { ...core, coreHash: digest(core) });
260
+ return { receipt: this.receipt(link), bundle };
261
+ });
262
+ }
263
+
264
+ receipt(link) { return { linkId: link.id, goal: { id: link.goalId, revision: link.goalRevision,
265
+ definitionVersion: link.definitionVersion, contentHash: link.contentHash }, authority: 'association-only; contribution-not-assessed' }; }
266
+ emptyBundle({ sessionId, turn, startSeq }) {
267
+ return { version: 'arcfold-linked-goal-v1', sessionId, turn, startSeq, links: [],
268
+ authority: 'program-provided goal evidence; no-new-work-or-user-approval' };
269
+ }
270
+ assertCore(link) {
271
+ const { revision, createdAt, updatedAt, createdSequence, updatedSequence, coreHash, ...core } = link;
272
+ if (revision !== 1 || createdSequence !== updatedSequence || digest(core) !== coreHash)
273
+ fail('goal_link_changed', '原目标关联不可改写。');
274
+ }
275
+ assertCurrent({ linkId, sessionId, turn, startSeq }) {
276
+ position({ sessionId, turn, startSeq });
277
+ const link = this.store.get('goal_links', linkId);
278
+ if (!link || link.sessionId !== sessionId || link.turn !== turn || link.startSeq !== startSeq ||
279
+ this.store.get('goal_link_outcomes', linkId)) fail('goal_link_closed', '目标关联不属于当前未结束轮次。');
280
+ this.assertCore(link);
281
+ const goal = this.store.get('goals', link.goalId), snapshot = this.store.get('context_snapshots', link.contextSnapshotId);
282
+ const shown = snapshot?.material?.goals?.find(row => row.id === link.goalId);
283
+ if (goal && !shown?.repairOnly && !goalDefinitionCurrent(this.store, goal))
284
+ fail('goal_unavailable', '目标定义或其原件已失效。');
285
+ if (!goal || !shown || (shown.repairOnly
286
+ ? goal.revision !== link.goalRevision || goalContentHash(goal) !== link.contentHash
287
+ : !frozenGoalCurrent(this.store, { ...shown, revision: link.goalRevision, contentHash: link.contentHash },
288
+ snapshot.material.cutoffSequence))) fail('goal_selection_changed', '关联的目标定义或控制状态已变化。');
289
+ if (shown.repairOnly) {
290
+ const entry = { linkId, ...this.fullEntry(goal, shown, link.selection, snapshot) };
291
+ if (canonicalJson(evidenceView(entry)) !== canonicalJson(evidenceView(link.bundle)))
292
+ fail('goal_source_changed', '目标修复材料已经变化。');
293
+ } else if (![link.bundle.goal, ...link.bundle.relatedGoals].every(row => frozenGoalCurrent(this.store, row)) ||
294
+ !link.bundle.sources.every(ref => this.store.get('events', ref.id)?.revision === ref.revision &&
295
+ sourceAvailable(this.store, this.store.get('events', ref.id))) ||
296
+ !link.dependencyRefs.every(ref => {
297
+ const row = this.store.get(ref.kind, ref.id);
298
+ return row?.revision === ref.revision && dependenciesCurrent(this.store, row, this.store.currentSequence());
299
+ })) fail('goal_source_changed', '目标完整材料的来源或依赖已经变化。');
300
+ // assertCore above still verifies the exact historical bytes. Only the new
301
+ // model-facing view drops transport metadata; the saved link is unchanged.
302
+ return { link, bundle: evidenceView(link.bundle) };
303
+ }
304
+ currentBundle({ sessionId, turn, startSeq }) {
305
+ position({ sessionId, turn, startSeq });
306
+ const bundle = this.emptyBundle({ sessionId, turn, startSeq });
307
+ for (const link of this.store.listAll('goal_links', { where: { sessionId, startSeq } })) {
308
+ if (link.turn !== turn) fail('goal_link_conflict', '同一开始位置不能对应不同轮次。');
309
+ if (this.store.get('goal_link_outcomes', link.id)) continue;
310
+ try { bundle.links.push(this.assertCurrent({ linkId: link.id, sessionId, turn, startSeq }).bundle); }
311
+ catch (error) { if (!(error instanceof DomainError)) throw error; }
312
+ }
313
+ return bundle;
314
+ }
315
+
316
+ recordInput({ linkIds, sessionId, turn, startSeq, step, requestIdentity, inputHash }) {
317
+ position({ sessionId, turn, startSeq }); identity(requestIdentity, 'Native request identity');
318
+ if (!nonnegative(step) || !Array.isArray(linkIds) || linkIds.length !== 1 || !/^[a-f0-9]{64}$/.test(inputHash))
319
+ fail('invalid_goal_input', '需要本轮唯一关联及完整模型背景摘要。');
320
+ return this.store.transaction(() => linkIds.map(linkId => {
321
+ this.assertCurrent({ linkId, sessionId, turn, startSeq });
322
+ const record = { id: 'goal-input-' + digest([linkId, requestIdentity]), linkId, sessionId, turn, startSeq,
323
+ step, requestIdentity, inputHash, evidence: 'validated-input-handed-to-host; actual-send-requires-native-request-evidence' };
324
+ const old = this.store.get('goal_inputs', record.id);
325
+ if (old) {
326
+ if (old.inputHash !== inputHash || old.step !== step) fail('goal_input_conflict', '同一发送身份不能更换目标背景。');
327
+ return old;
328
+ }
329
+ return this.store.save('goal_inputs', record);
330
+ }));
331
+ }
332
+
333
+ settleTurn({ sessionId, turn, startSeq, outcome, deliveryId = null }) {
334
+ position({ sessionId, turn, startSeq });
335
+ if (!['completed', 'cancelled', 'failed', 'interrupted', 'unknown'].includes(outcome)) fail('invalid_goal_outcome', '需要真实宿主终态。');
336
+ return this.store.transaction(() => {
337
+ const delivery = deliveryId && this.store.get('deliveries', deliveryId);
338
+ if (deliveryId && (!delivery || delivery.sessionId !== sessionId || delivery.turn !== turn || delivery.startSeq !== startSeq))
339
+ fail('goal_delivery_mismatch', '交付不属于关联的原生轮次。');
340
+ return this.store.listAll('goal_links', { where: { sessionId, startSeq } }).map(link => {
341
+ this.assertCore(link);
342
+ if (link.turn !== turn) fail('goal_link_conflict', '关联轮次不匹配。');
343
+ const record = { id: link.id, linkId: link.id, coreHash: link.coreHash, sessionId, turn, startSeq,
344
+ outcome, deliveryId, endEventId: delivery?.endEventId ?? null, contributionEffect: 'not_assessed' };
345
+ const old = this.store.get('goal_link_outcomes', link.id);
346
+ if (old) {
347
+ if (old.outcome !== outcome || old.deliveryId !== deliveryId) fail('goal_outcome_conflict', '原生结束事实不可改换。');
348
+ return old;
349
+ }
350
+ return this.store.save('goal_link_outcomes', record);
351
+ });
352
+ });
353
+ }
354
+
355
+ details(goalId, { sessionId, viewScope } = {}) {
356
+ identity(goalId, 'Goal id'); this.reconcile();
357
+ const all = browseScope(viewScope) === 'all';
358
+ const goal = this.store.get('goals', goalId);
359
+ if (!goal || !all && sessionId && !inScope(goal, sessionId)) fail('goal_not_found', '目标不存在或不在当前讨论范围。');
360
+ return this.detailView(goal, { sessionId, viewScope });
361
+ }
362
+
363
+ contribution(link) {
364
+ const unavailable = { available: false, message: '当时关联判断的原件或依赖已失效,或缺少可复核版本,暂不展示旧正文。' };
365
+ try { this.assertCore(link); } catch (error) { if (error instanceof DomainError) return unavailable; throw error; }
366
+ const currentGoal = this.store.get('goals', link.goalId);
367
+ if (!currentGoal || currentGoal.deleted || currentGoal.withdrawn || currentGoal.useProhibited ||
368
+ !Array.isArray(link.dependencyRefs) || !link.bundle.sources.every(shown => {
369
+ const source = this.store.get('events', shown.id);
370
+ return source?.revision === shown.revision && sourceAvailable(this.store, source);
371
+ }) || !link.dependencyRefs.every(ref => {
372
+ if (!['understandings', 'methods'].includes(ref.kind)) return false;
373
+ const dependency = this.store.get(ref.kind, ref.id);
374
+ return dependency?.revision === ref.revision && dependenciesCurrent(this.store, dependency, this.store.currentSequence());
375
+ })) return unavailable;
376
+ return { available: true, reason: link.selection.reason, claimType: 'inference',
377
+ sourceIds: [...link.selection.sourceIds], definitionVersion: link.definitionVersion };
378
+ }
379
+
380
+ /** Pure projection after the caller has reconciled source-invalid goals once. */
381
+ detailView(goal, { sessionId, viewScope, visibleWorks } = {}) {
382
+ const goalId = goal.id;
383
+ const all = browseScope(viewScope) === 'all';
384
+ const projected = this.view(goal), allowed = Boolean(projected);
385
+ const discussions = this.store.listAll('goal_discussions', { where: { goalId } })
386
+ .filter(row => all || !sessionId || row.sessionId === sessionId).reverse();
387
+ return { goal: projected?.goal || metadata(goal), contentHash: goalContentHash(goal),
388
+ browse: browseContext(goal, sessionId),
389
+ sources: projected?.sources || [], relations: projected?.goal.relations || [], coverage: projected?.coverage || {},
390
+ availability: { allowed, ...(!allowed ? { code: 'goal_source_invalid', message: '目标依据失效,旧内容不再提供。' } : {}) },
391
+ repairAllowed: goalRepairAllowed(goal), controls: { resumeAllowed: allowed },
392
+ discussionsTotal: discussions.length, discussions: discussions.slice(0, 20).map(row => ({ id: row.id,
393
+ sessionId: row.sessionId, requestId: row.requestId, mode: row.mode, content: row.request.content,
394
+ admission: row.admission ?? null, createdAt: row.createdAt })),
395
+ works: visibleWorks ?? (all ? this.browseWorks(sessionId, goalId)
396
+ : sessionId ? this.works(sessionId, goalId, viewScope === undefined ? 20 : null) : []),
397
+ links: this.store.listAll('goal_links', { where: { goalId } }).map(link => ({ id: link.id, sessionId: link.sessionId,
398
+ turn: link.turn, definitionVersion: link.definitionVersion,
399
+ contribution: this.contribution(link),
400
+ outcome: this.store.get('goal_link_outcomes', link.id) })) };
401
+ }
402
+ list({ sessionId, viewScope, offset = 0, limit = 20 } = {}) {
403
+ paging(offset, limit); this.reconcile();
404
+ const all = browseScope(viewScope) === 'all';
405
+ const rows = this.store.listAll('goals').filter(goal => all || !sessionId || inScope(goal, sessionId)).reverse();
406
+ const works = all ? this.browseWorks(sessionId) : sessionId ? this.works(sessionId, null, viewScope === undefined ? 20 : null) : [];
407
+ return { works: viewScope === undefined ? works : works.filter(work => work.relation?.state !== 'current'),
408
+ items: rows.slice(offset, offset + limit).map(goal => this.detailView(goal, { sessionId, viewScope,
409
+ ...(viewScope === undefined ? {} : { visibleWorks: works.filter(work => work.relation?.goalId === goal.id) }) })), total: rows.length,
410
+ nextOffset: offset + limit < rows.length ? offset + limit : null };
411
+ }
412
+
413
+ // Read the same persisted work set from every chat; controls still belong to
414
+ // the original chat and must pass the existing native command checks.
415
+ browseWorks(sessionId, goalId = null) {
416
+ // roots() is a read of already-live top-level Agents, never a wake/load.
417
+ const live = this.nativeRoot?.get('agents')?.roots?.() ?? [];
418
+ const sessions = [...new Set([...this.store.listAll('goal_works').map(work => work.sessionId),
419
+ ...live.map(agent => agent.id), ...(sessionId ? [sessionId] : [])])];
420
+ return sessions.flatMap(origin => this.works(origin, goalId, null).map(work => origin === sessionId ? work
421
+ : { ...work, canStop: false, canResume: false, otherSession: true }));
422
+ }
423
+
424
+
425
+ async controlWork({ sessionId, workId: id, nativeGoalId, nativeRevision, requestId, userIdentity, action, agent }) {
426
+ if (!['pause', 'resume'].includes(action) || id !== nativeWorkId(sessionId, nativeGoalId) || !positive(nativeRevision))
427
+ fail('invalid_goal_work_control', '工作控制须对应准确的原生工作版本。');
428
+ identity(requestId, 'Work control request'); identity(userIdentity, 'User identity');
429
+ const runtime = this.nativeRoot;
430
+ if (!runtime || runtime.get('agents')?.get(sessionId) !== agent)
431
+ fail('goal_main_session_required', '当前主交流已经变化。');
432
+ const native = runtime.get('goals');
433
+ const request = { sessionId, workId: id, nativeGoalId, nativeRevision, requestId, userIdentity, action };
434
+ const decisionId = 'work-control-' + digest([userIdentity, requestId]);
435
+ let prior = this.store.get('goal_work_controls', decisionId);
436
+ const current = () => native.get(agent) ?? null;
437
+ if (prior) {
438
+ if (canonicalJson(prior.request) !== canonicalJson(request)) fail('decision_identity_conflict', '同一工作决定不能更换内容。');
439
+ if (prior.state === 'applied') return { state: 'applied', decisionId, native: current() };
440
+ const event = agent.session.snapshotEvents().find(e => e.seq === prior.nativeSeq);
441
+ if (event?.type === 'goal/change' && event.data.operation === action &&
442
+ event.data.goal?.id === nativeGoalId && event.data.goal.revision === nativeRevision + 1 &&
443
+ (action !== 'resume' || !this.execution || this.execution.browserControlSaved(agent, decisionId, request)) &&
444
+ await runtime.get('sessions').flush(agent.session) === true) {
445
+ const latestDecision = this.store.get('goal_work_controls', decisionId);
446
+ if (latestDecision.state !== 'applied') this.store.save('goal_work_controls',
447
+ { ...latestDecision, state: 'applied', nativeEventHash: digest(event) }, { expectedRevision: latestDecision.revision });
448
+ return { state: 'applied', decisionId, native: current() };
449
+ }
450
+ return { state: 'unknown', decisionId, native: current() };
451
+ }
452
+ const before = current();
453
+ if (!before || before.id !== nativeGoalId || before.revision !== nativeRevision)
454
+ fail('revision_conflict', '执行已变化,请刷新后再决定。');
455
+ if (action === 'pause' && before.phase !== 'active' || action === 'resume' &&
456
+ (!(['paused', 'blocked'].includes(before.phase) || before.phase === 'active' && before.activation === 'disarmed') ||
457
+ before.roundsStarted >= before.maxGoalRounds))
458
+ fail('goal_work_not_resumable', '当前工作状态不允许这个控制,请查看实际进展。');
459
+ if (action === 'resume') {
460
+ if (this.execution) this.execution.assertNative(this.execution.current(agent), before);
461
+ else assertWork(this.store, this.store.get('goal_works', id));
462
+ }
463
+ // Persist the decision before entering the native side. Unknown is never replayed.
464
+ prior = this.store.save('goal_work_controls', { id: decisionId, request, state: 'pending', nativeSeq: agent.session.seq,
465
+ provenance: { kind: 'authenticated_browser_work_control', userIdentity } });
466
+ const finishControl = action === 'resume' && this.execution ? this.execution.beginControl(agent) : () => {};
467
+ try {
468
+ if (action === 'resume' && this.execution)
469
+ this.execution.resume(agent, { goalId: nativeGoalId, revision: nativeRevision, userDecision: decisionId });
470
+ else native[action](agent, { id: nativeGoalId, revision: nativeRevision });
471
+ if (await runtime.get('sessions').flush(agent.session) !== true) throw new Error('Native work control is not durable');
472
+ const event = agent.session.snapshotEvents().find(e => e.seq === prior.nativeSeq);
473
+ if (event?.type !== 'goal/change' || event.data.operation !== action ||
474
+ event.data.goal?.id !== nativeGoalId || event.data.goal.revision !== nativeRevision + 1 ||
475
+ action === 'resume' && this.execution && !this.execution.browserControlSaved(agent, decisionId, request))
476
+ throw new Error('Native work control has no exact receipt');
477
+ const latestDecision = this.store.get('goal_work_controls', decisionId);
478
+ if (latestDecision.state !== 'applied') this.store.save('goal_work_controls',
479
+ { ...latestDecision, state: 'applied', nativeEventHash: digest(event) }, { expectedRevision: latestDecision.revision });
480
+ return { state: 'applied', decisionId, native: current() };
481
+ } catch {
482
+ // A pending receipt remains inspectable if the business write itself failed.
483
+ if (action === 'resume') this.execution?.abortControl(agent, decisionId);
484
+ return { state: 'unknown', decisionId, native: current() };
485
+ } finally { finishControl(); }
486
+ }
487
+
488
+ works(sessionId, goalId = null, limit = 20) {
489
+ const agent = this.nativeRoot?.get('agents')?.get(sessionId);
490
+ const native = agent && this.nativeRoot?.get('goals')?.get(agent);
491
+ return workViews(this.store, sessionId, native, goalId, limit).map(work => {
492
+ if (!this.execution) return work;
493
+ const stored = this.store.listAll('execution_stages', { where: { sessionId, nativeGoalId: work.nativeGoalId } })[0];
494
+ const stage = this.execution.view(stored);
495
+ return { ...work, stage, canResume: Boolean(native?.id === work.nativeGoalId &&
496
+ (['paused', 'blocked'].includes(native.phase) || native.phase === 'active' && native.activation === 'disarmed') &&
497
+ native.roundsStarted < native.maxGoalRounds && stage?.available && ['waiting', 'paused', 'running'].includes(stage.status)) };
498
+ });
499
+ }
500
+
501
+ /** A browser choice is inert until the exact official user original exists. */
502
+ bindDiscussion({ sessionId, requestId, content, goalId, expectedRevision, definitionVersion, mode, userIdentity }) {
503
+ identity(sessionId, 'Native session id'); identity(requestId, 'Official prompt request id');
504
+ identity(goalId, 'Goal id'); identity(userIdentity, 'Trusted browser identity');
505
+ if (!positive(expectedRevision) || !positive(definitionVersion) || !['discuss', 'repair'].includes(mode) ||
506
+ !Array.isArray(content) || content.length !== 1 || content[0]?.type !== 'text' ||
507
+ typeof content[0].text !== 'string' || !content[0].text.trim() ||
508
+ Object.keys(content[0]).some(key => !['type', 'text'].includes(key)))
509
+ fail('invalid_goal_discussion', '讨论选择须绑定明确版本及随后实际提交的完整文字。');
510
+ const request = { sessionId, requestId, content: copy(content), goalId, expectedRevision,
511
+ definitionVersion, mode, userIdentity };
512
+ this.reconcile();
513
+ return this.store.transaction(() => {
514
+ const id = 'goal-discussion-' + digest([sessionId, requestId]), old = this.store.get('goal_discussions', id);
515
+ const goalRef = { id: goalId, revision: expectedRevision, definitionVersion, mode };
516
+ if (old) {
517
+ if (old.requestHash !== digest(request) || canonicalJson(old.request) !== canonicalJson(request))
518
+ fail('discussion_identity_conflict', '同一原生发送编号不能换目标、原话或用户身份。');
519
+ return { binding: old, goalRef, created: false };
520
+ }
521
+ const goal = this.store.get('goals', goalId);
522
+ if (!goal || goal.revision !== expectedRevision || goal.definitionVersion !== definitionVersion)
523
+ fail('revision_conflict', '目标已变化,请重新查看后选回。');
524
+ if (!inScope(goal, sessionId)) fail('goal_scope_denied', '选择不能扩大目标原有讨论范围。');
525
+ if (mode === 'repair' ? !goalRepairAllowed(goal) : !goalDefinitionCurrent(this.store, goal))
526
+ fail('goal_discussion_unavailable', '目标不具备所选讨论或重新澄清资格。');
527
+ const binding = this.store.save('goal_discussions', { id, sessionId, requestId, contentHash: digest(content),
528
+ goalId, goalRevision: expectedRevision, definitionVersion, mode, userIdentity, request,
529
+ requestHash: digest(request), actor: 'user', provenance: { kind: 'trusted_user_goal_discussion', userIdentity },
530
+ authority: 'chosen-for-one-exact-user-prompt; not-a-prompt-or-execution-record' });
531
+ return { binding, goalRef, created: true };
532
+ });
533
+ }
534
+
535
+ recordDiscussionAdmission({ bindingId, state }) {
536
+ if (!['accepted', 'failed', 'unknown'].includes(state)) fail('invalid_discussion_admission', '需要实际提交入口的已知结果或未知。');
537
+ return this.store.transaction(() => {
538
+ const binding = this.store.get('goal_discussions', bindingId);
539
+ if (!binding || digest(binding.request) !== binding.requestHash) fail('goal_discussion_changed', '讨论绑定缺失或改变。');
540
+ if (binding.admission) {
541
+ if (binding.admission.state !== state) fail('discussion_admission_conflict', '提交回执已保存,不可改换或重发。');
542
+ return binding;
543
+ }
544
+ return this.store.save('goal_discussions', { ...binding, admission: { state,
545
+ evidence: 'official-prompt-entry-result; not-proof-of-persisted-user-message' } }, { expectedRevision: binding.revision });
546
+ });
547
+ }
548
+
549
+ decide({ goalId, expectedRevision, definitionVersion, action, requestId, userIdentity, sessionId }) {
550
+ identity(goalId, 'Goal id'); identity(requestId, 'Goal decision request'); identity(userIdentity, 'Trusted user identity');
551
+ if (!positive(expectedRevision) || !positive(definitionVersion) || !['pause', 'resume', 'close'].includes(action))
552
+ fail('invalid_goal_decision', '用户目标决定须绑定所见版本和明确动作。');
553
+ const request = { goalId, expectedRevision, definitionVersion, action, requestId, userIdentity, ...(sessionId ? { sessionId } : {}) };
554
+ this.reconcile();
555
+ const result = this.store.transaction(() => {
556
+ const id = 'goal-decision-' + digest([userIdentity, requestId]), prior = this.store.get('goal_decisions', id);
557
+ if (prior) {
558
+ if (canonicalJson(prior.request) !== canonicalJson(request)) fail('decision_identity_conflict', '同一决定身份不能更换内容。');
559
+ return { goal: prior.goal, decision: prior };
560
+ }
561
+ const goal = this.store.get('goals', goalId);
562
+ if (!goal || goal.revision !== expectedRevision || goal.definitionVersion !== definitionVersion)
563
+ fail('revision_conflict', '目标已变化,请重新查看后决定。');
564
+ if (goal.deleted || goal.withdrawn) fail('goal_unavailable', '撤回或删除目标不能改变追踪状态。');
565
+ if (action === 'resume' && !goalDefinitionCurrent(this.store, goal)) fail('goal_source_invalid', '原件失效的目标不能恢复追踪。');
566
+ const saved = saveGoalTransition(this.store, goal, { ...goal, status: { pause: 'paused', resume: 'tracking', close: 'closed' }[action],
567
+ userControl: { action, definitionVersion, decisionId: id, actor: 'user', userIdentity,
568
+ ...(sessionId && this.nativeRoot?.get('agents')?.get(sessionId) ? { nativeBoundary: { sessionId,
569
+ throughSeq: this.nativeRoot.get('agents').get(sessionId).session.seq - 1 } } : {}) } },
570
+ { commandId: id, actor: 'trusted_user_goal_decision', pendingId: null, reason: action });
571
+ const decision = this.store.save('goal_decisions', { id, request, goal: saved, actor: 'user',
572
+ provenance: { kind: 'trusted_user_goal_decision', userIdentity }, meaning: action === 'close' ? 'end-tracking; not-proof-of-achievement' : action });
573
+ return { goal: saved, decision };
574
+ });
575
+ if (action !== 'resume') this.execution?.stopForGoal(goalId);
576
+ return result;
577
+ }
578
+ }