@mokuyoaxis/dsh-iris 0.1.4 → 0.2.1

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 (110) hide show
  1. package/.dsh/skills/iris-compose-media/SKILL.md +10 -5
  2. package/.dsh/skills/iris-compose-media/references/workflows.md +7 -1
  3. package/.dsh/skills/iris-verify-ui/SKILL.md +2 -1
  4. package/CHANGELOG.md +99 -1
  5. package/README.en.md +124 -31
  6. package/README.md +124 -31
  7. package/bin/dsh-iris.js +593 -13
  8. package/docs/AGENT_QUICKSTART.md +39 -0
  9. package/docs/ARCHITECTURE.md +87 -64
  10. package/docs/ARTIFACT_LIBRARY.md +5 -5
  11. package/docs/ARTIFACT_MANIFEST.md +60 -0
  12. package/docs/CLI_MANAGEMENT.md +181 -0
  13. package/docs/COMPOSITE_VISION.md +35 -0
  14. package/docs/CORE_RUNTIME_CONTRACT.md +84 -0
  15. package/docs/DSH_CORE_MIGRATION.md +135 -0
  16. package/docs/DSH_RC2_ADAPTATION.md +38 -0
  17. package/docs/FAULT_INJECTION.md +12 -2
  18. package/docs/HEADLESS_CLI.md +223 -0
  19. package/docs/HOST_ADAPTER_CONTRACT.md +36 -17
  20. package/docs/HOST_DOCTOR.md +4 -2
  21. package/docs/MODEL_PORT_CONTRACT.md +305 -0
  22. package/docs/OCR_MODEL.md +39 -0
  23. package/docs/PROMPT_OPTIMIZER.md +47 -0
  24. package/docs/PROVIDER_ADAPTER_CONTRACT.md +53 -10
  25. package/docs/PROVIDER_HEALTH.md +22 -5
  26. package/docs/PROVIDER_RUNTIME_HARNESS.md +33 -0
  27. package/docs/PROVIDER_SUBMISSION_CONTRACT.md +2 -2
  28. package/docs/ROADMAP.md +49 -83
  29. package/docs/SECURITY.md +8 -7
  30. package/docs/TASK_SEMANTICS.md +2 -2
  31. package/docs/VISION_CLI.md +132 -0
  32. package/docs/VISION_MODEL.md +51 -0
  33. package/docs/WORKBENCH_ARTIFACTS.md +67 -0
  34. package/docs/assets/logo/README.md +55 -0
  35. package/docs/assets/logo/iris-bubble-concept.svg +67 -0
  36. package/docs/assets/logo/iris-bubble.svg +14 -0
  37. package/docs/assets/logo/iris-interim-flower.png +0 -0
  38. package/docs/assets/logo/iris-logo-mono-inverse.svg +78 -0
  39. package/docs/assets/logo/iris-logo-mono.svg +78 -0
  40. package/docs/assets/logo/iris-logo.svg +78 -0
  41. package/docs/assets/logo/iris-mark-mono-inverse.svg +42 -0
  42. package/docs/assets/logo/iris-mark-mono.svg +42 -0
  43. package/docs/assets/logo/iris-mark.svg +42 -0
  44. package/docs/file-access-across-environments.md +8 -5
  45. package/docs/releases/0.2.0.md +48 -0
  46. package/docs/releases/0.2.1.md +56 -0
  47. package/lib/actions.js +520 -591
  48. package/lib/adapters.js +184 -36
  49. package/lib/api.js +200 -11
  50. package/lib/chromium-browser.js +117 -0
  51. package/lib/client.js +1390 -105
  52. package/lib/command-service.js +534 -0
  53. package/lib/composite-vision-routing.js +42 -0
  54. package/lib/config.js +143 -51
  55. package/lib/core-artifact-store.js +630 -0
  56. package/lib/core-artifacts.js +4 -0
  57. package/lib/core-attention.js +103 -0
  58. package/lib/core-contract.js +105 -0
  59. package/lib/core-doctor.js +41 -0
  60. package/lib/core-lease-recovery.js +170 -0
  61. package/lib/core-maintenance.js +213 -0
  62. package/lib/core-runtime.js +232 -0
  63. package/lib/core-tasks.js +567 -0
  64. package/lib/core-user-projection.js +193 -0
  65. package/lib/doctor.js +92 -33
  66. package/lib/dsh-core-adapter.js +653 -0
  67. package/lib/dsh-host-adapter.js +58 -7
  68. package/lib/dsh-text-model-adapter.js +222 -0
  69. package/lib/dsh-vision-model-adapter.js +75 -0
  70. package/lib/generation-input.js +126 -0
  71. package/lib/guard.js +3 -3
  72. package/lib/headless-vision.js +181 -0
  73. package/lib/host-runtime.js +1 -0
  74. package/lib/http-vision-model-adapter.js +187 -0
  75. package/lib/image-edit-input.js +34 -0
  76. package/lib/index.js +323 -188
  77. package/lib/locate.js +86 -28
  78. package/lib/media-probe.js +2 -1
  79. package/lib/model-call-runtime.js +45 -0
  80. package/lib/model-invoker.js +220 -0
  81. package/lib/model-port-contract.js +341 -0
  82. package/lib/models.js +105 -14
  83. package/lib/ocr-model-routing.js +26 -0
  84. package/lib/ocr.js +110 -64
  85. package/lib/prompt-optimizer-config.js +2 -1
  86. package/lib/prompt-optimizer-core.js +121 -0
  87. package/lib/prompt-optimizer.js +37 -193
  88. package/lib/provider-adapter.js +49 -23
  89. package/lib/provider-adapters.js +165 -26
  90. package/lib/provider-catalog.js +183 -0
  91. package/lib/provider-config-service.js +236 -0
  92. package/lib/provider-contract.js +74 -4
  93. package/lib/provider-health.js +80 -1
  94. package/lib/provider-protocol.js +74 -2
  95. package/lib/provider-task-runner.js +370 -0
  96. package/lib/summarize.js +31 -20
  97. package/lib/summary-input.js +52 -0
  98. package/lib/task-wait.js +40 -0
  99. package/lib/tasks.js +16 -4
  100. package/lib/video-input.js +30 -0
  101. package/lib/vision-core.js +42 -0
  102. package/lib/vision-image-input.js +83 -0
  103. package/lib/vision-input.js +31 -0
  104. package/lib/vision-model-routing.js +180 -0
  105. package/lib/vision.js +31 -9
  106. package/lib/workbench-artifacts.js +52 -0
  107. package/lib/workbench-management.js +151 -0
  108. package/lib/workbench-zip.js +39 -0
  109. package/package.json +8 -8
  110. package/user_guide.md +71 -26
@@ -0,0 +1,567 @@
1
+ 'use strict';
2
+
3
+ import crypto from 'node:crypto';
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import { atomicWritePrivate, ensurePrivateDir } from './private-storage.js';
7
+ import { parseModelRef } from './models.js';
8
+ import { providerErrorRecord } from './provider-contract.js';
9
+ import { deriveLegacyStatus, semanticViolations } from './task-semantics.js';
10
+
11
+ const STORE_DIR = 'task-store';
12
+ const STORE_VERSION = 'v0';
13
+ const TASK_ID = /^task_[a-f0-9]{24}$/;
14
+ const ARTIFACT_ID = /^artifact_[a-f0-9]{24}$/;
15
+ const ATTEMPT_ID = /^attempt_[a-f0-9]{24}$/;
16
+ const PROVIDER_BINDING = /^sha256:[a-f0-9]{64}$/;
17
+ const CAPABILITIES = new Set(['image', 'video', 'tts', 'transcribe']);
18
+ const ATTEMPT_SELECTION_REASONS = new Set(['explicit', 'assignment', 'pool']);
19
+ const ATTEMPT_STAGE_TIMESTAMPS = Object.freeze([
20
+ 'queuedAt', 'submitStartedAt', 'submittedAt', 'remoteCompletedAt',
21
+ 'downloadStartedAt', 'downloadedAt', 'localProcessedAt'
22
+ ]);
23
+
24
+ export class CoreTaskError extends Error {
25
+ constructor(code, message) {
26
+ super(message);
27
+ this.name = 'CoreTaskError';
28
+ this.code = code;
29
+ }
30
+ }
31
+
32
+ function fail(code, message) {
33
+ throw new CoreTaskError(code, message);
34
+ }
35
+
36
+ function taskDirectory(dataRoot) {
37
+ return path.join(dataRoot, STORE_DIR, STORE_VERSION, 'tasks');
38
+ }
39
+
40
+ function taskFile(dataRoot, id) {
41
+ const stableId = String(id || '');
42
+ if (!TASK_ID.test(stableId)) fail('IRIS_TASK_ID_INVALID', 'Task ID 格式无效');
43
+ return path.join(taskDirectory(dataRoot), stableId + '.json');
44
+ }
45
+
46
+ function clone(value) {
47
+ return structuredClone(value);
48
+ }
49
+
50
+ function publicTask(record) {
51
+ return Object.freeze(clone(record));
52
+ }
53
+
54
+ function validIsoTimestamp(value) {
55
+ if (typeof value !== 'string') return false;
56
+ const millis = Date.parse(value);
57
+ return Number.isFinite(millis) && new Date(millis).toISOString() === value;
58
+ }
59
+
60
+ function validAttemptStageTimestamps(value) {
61
+ if (value === undefined) return true;
62
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
63
+ const keys = Object.keys(value);
64
+ if (!keys.length || keys.some((key) => !ATTEMPT_STAGE_TIMESTAMPS.includes(key))) return false;
65
+ let previous = -Infinity;
66
+ let gap = false;
67
+ for (const key of ATTEMPT_STAGE_TIMESTAMPS) {
68
+ if (!Object.prototype.hasOwnProperty.call(value, key)) {
69
+ gap = true;
70
+ continue;
71
+ }
72
+ if (gap || !validIsoTimestamp(value[key])) return false;
73
+ const current = Date.parse(value[key]);
74
+ if (current < previous) return false;
75
+ previous = current;
76
+ }
77
+ return true;
78
+ }
79
+
80
+ function timestampAfter(...values) {
81
+ const floor = values.reduce((latest, value) => {
82
+ const parsed = Date.parse(String(value || ''));
83
+ return Number.isFinite(parsed) ? Math.max(latest, parsed) : latest;
84
+ }, 0);
85
+ return new Date(Math.max(Date.now(), floor)).toISOString();
86
+ }
87
+
88
+ function activeAttempt(task) {
89
+ return task.attempts.find((item) => item.id === task.activeAttemptId);
90
+ }
91
+
92
+ function appendAttemptStage(attempt, key, reference) {
93
+ if (!attempt?.stageTimestamps) return undefined;
94
+ if (attempt.stageTimestamps[key]) return attempt.stageTimestamps[key];
95
+ const previousKey = ATTEMPT_STAGE_TIMESTAMPS[ATTEMPT_STAGE_TIMESTAMPS.indexOf(key) - 1];
96
+ if (previousKey && !attempt.stageTimestamps[previousKey]) return undefined;
97
+ const stamped = timestampAfter(attempt.stageTimestamps[previousKey], reference);
98
+ attempt.stageTimestamps[key] = stamped;
99
+ return stamped;
100
+ }
101
+
102
+ function validateRecord(record, expectedId) {
103
+ if (!record || typeof record !== 'object' || Array.isArray(record)
104
+ || record.schemaVersion !== 0 || record.semanticsVersion !== 2
105
+ || record.id !== expectedId || !CAPABILITIES.has(record.capability)
106
+ || !Array.isArray(record.attempts) || !Array.isArray(record.artifactIds)
107
+ || !Number.isSafeInteger(record.revision) || record.revision < 1
108
+ || typeof record.createdAt !== 'string' || typeof record.updatedAt !== 'string'
109
+ || (record.providerBinding !== undefined && !PROVIDER_BINDING.test(record.providerBinding))
110
+ || (record.retriedFrom !== undefined && !TASK_ID.test(record.retriedFrom))
111
+ || (record.sourceArtifactId !== undefined && (record.capability !== 'image' || !ARTIFACT_ID.test(record.sourceArtifactId)))) {
112
+ fail('IRIS_TASK_INVALID', 'Task 记录结构无效');
113
+ }
114
+ if (record.attempts.some((attempt, index) => !attempt || !ATTEMPT_ID.test(attempt.id)
115
+ || attempt.ordinal !== index + 1 || typeof attempt.providerId !== 'string'
116
+ || typeof attempt.model !== 'string'
117
+ || (attempt.providerBinding !== undefined && !PROVIDER_BINDING.test(attempt.providerBinding))
118
+ || (attempt.selectionReason !== undefined && !ATTEMPT_SELECTION_REASONS.has(attempt.selectionReason))
119
+ || !validAttemptStageTimestamps(attempt.stageTimestamps))) {
120
+ fail('IRIS_TASK_INVALID', 'Task Attempt 记录结构无效');
121
+ }
122
+ const violations = semanticViolations(record);
123
+ if (violations.length) fail('IRIS_TASK_INVALID', 'Task 事实语义冲突:' + violations.join(', '));
124
+ return record;
125
+ }
126
+
127
+ function readRecord(dataRoot, id) {
128
+ const file = taskFile(dataRoot, id);
129
+ let source;
130
+ try {
131
+ const stat = fs.lstatSync(file);
132
+ if (!stat.isFile() || stat.isSymbolicLink()) fail('IRIS_TASK_INVALID', 'Task 记录不是普通文件');
133
+ source = fs.readFileSync(file, 'utf8');
134
+ } catch (error) {
135
+ if (error instanceof CoreTaskError) throw error;
136
+ fail('IRIS_TASK_NOT_FOUND', 'Task 不存在或记录无法读取');
137
+ }
138
+ let record;
139
+ try { record = JSON.parse(source); }
140
+ catch (_) { fail('IRIS_TASK_INVALID', 'Task 记录不是有效 JSON'); }
141
+ return validateRecord(record, id);
142
+ }
143
+
144
+ function writeRecord(dataRoot, record) {
145
+ validateRecord(record, record.id);
146
+ try {
147
+ ensurePrivateDir(taskDirectory(dataRoot));
148
+ atomicWritePrivate(taskFile(dataRoot, record.id), JSON.stringify(record, null, 2) + '\n');
149
+ } catch (error) {
150
+ if (error instanceof CoreTaskError) throw error;
151
+ fail('IRIS_TASK_WRITE_FAILED', 'Task 无法安全写入');
152
+ }
153
+ return publicTask(record);
154
+ }
155
+
156
+ function mutate(dataRoot, id, callback) {
157
+ const previous = readRecord(dataRoot, id);
158
+ const next = clone(previous);
159
+ callback(next);
160
+ next.revision = previous.revision + 1;
161
+ next.updatedAt = new Date().toISOString();
162
+ next.status = deriveLegacyStatus(next);
163
+ return writeRecord(dataRoot, next);
164
+ }
165
+
166
+ function safeError(error, overrides) {
167
+ return providerErrorRecord(error, overrides);
168
+ }
169
+
170
+ export function createCoreTask(dataRoot, input = {}) {
171
+ const capability = String(input.capability || '').trim();
172
+ if (!CAPABILITIES.has(capability)) fail('IRIS_TASK_INPUT_INVALID', 'Task capability 无效');
173
+ const retriedFrom = input.retriedFrom === undefined ? undefined : String(input.retriedFrom);
174
+ if (retriedFrom !== undefined && !TASK_ID.test(retriedFrom)) fail('IRIS_TASK_INPUT_INVALID', 'retriedFrom 必须是合法 Task ID');
175
+ const sourceArtifactId = input.sourceArtifactId;
176
+ if (sourceArtifactId !== undefined && (capability !== 'image' || !ARTIFACT_ID.test(sourceArtifactId))) fail('IRIS_TASK_INPUT_INVALID', '改图 sourceArtifactId 必须是合法 Artifact ID');
177
+ const now = new Date().toISOString();
178
+ return writeRecord(dataRoot, {
179
+ schemaVersion: 0,
180
+ semanticsVersion: 2,
181
+ id: 'task_' + crypto.randomBytes(12).toString('hex'),
182
+ capability,
183
+ status: 'running',
184
+ phase: 'queued',
185
+ acceptance: 'none',
186
+ watchState: 'idle',
187
+ outcome: 'none',
188
+ deliveryState: 'none',
189
+ cancelState: 'none',
190
+ attempts: [],
191
+ artifactIds: [],
192
+ revision: 1,
193
+ createdAt: now,
194
+ updatedAt: now,
195
+ // D4 retry as new task:显式单向关系(新任务指向旧任务),绝不复制 Prompt。
196
+ ...(retriedFrom !== undefined ? { retriedFrom } : {}),
197
+ ...(sourceArtifactId !== undefined ? { sourceArtifactId } : {})
198
+ });
199
+ }
200
+
201
+ export function inspectCoreTask(dataRoot, id) {
202
+ return publicTask(readRecord(dataRoot, id));
203
+ }
204
+
205
+ /**
206
+ * reader 列表不创建 task-store;存在的合法记录按更新时间倒序返回。
207
+ * skipInvalid 只服务用户侧只读投影:单条损坏记录跳过并计入 dropped,
208
+ * 其余记录照常返回;默认严格模式不变,损坏记录仍使整体读取失败。
209
+ */
210
+ export function listCoreTasks(dataRoot, { offset = 0, limit = 50, skipInvalid = false, status, capability, provider_id, model_ref, outcome, delivery_state } = {}) {
211
+ const start = Math.max(0, Math.trunc(Number(offset) || 0));
212
+ const size = Math.max(1, Math.min(200, Math.trunc(Number(limit) || 50)));
213
+ const directory = taskDirectory(dataRoot);
214
+ let names;
215
+ for (const managed of [
216
+ path.join(dataRoot, STORE_DIR), path.join(dataRoot, STORE_DIR, STORE_VERSION), directory
217
+ ]) {
218
+ let stat;
219
+ try { stat = fs.lstatSync(managed); }
220
+ catch (error) {
221
+ if (error?.code === 'ENOENT') {
222
+ return Object.freeze({
223
+ schemaVersion: 0, total: 0, offset: start, limit: size, dropped: 0,
224
+ tasks: Object.freeze([])
225
+ });
226
+ }
227
+ fail('IRIS_TASK_STORE_INVALID', 'Task Store 无法读取');
228
+ }
229
+ if (!stat.isDirectory() || stat.isSymbolicLink()) fail('IRIS_TASK_STORE_INVALID', 'Task Store 目录不安全');
230
+ }
231
+ try { names = fs.readdirSync(directory); }
232
+ catch (_) { fail('IRIS_TASK_STORE_INVALID', 'Task Store 无法读取'); }
233
+ const records = [];
234
+ let dropped = 0;
235
+ for (const name of names) {
236
+ if (!/^task_[a-f0-9]{24}\.json$/.test(name)) continue;
237
+ try { records.push(readRecord(dataRoot, name.slice(0, -5))); }
238
+ catch (error) {
239
+ if (!skipInvalid) throw error;
240
+ dropped += 1;
241
+ }
242
+ }
243
+ records.sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt)));
244
+ const matches = records.filter(record => Object.entries({ status, capability, providerId: provider_id, modelRef: model_ref, outcome, deliveryState: delivery_state })
245
+ .every(([field, value]) => value === undefined || record[field] === value));
246
+ return Object.freeze({
247
+ schemaVersion: 0, total: matches.length, offset: start, limit: size, dropped,
248
+ tasks: Object.freeze(matches.slice(start, start + size).map(publicTask))
249
+ });
250
+ }
251
+
252
+ /** 必须在调用 Provider submit 之前完成。 */
253
+ export function beginCoreAttempt(dataRoot, taskId, fields = {}) {
254
+ const providerId = String(fields.providerId || '').trim();
255
+ const model = String(fields.model || '').trim();
256
+ const parsed = parseModelRef(model);
257
+ if (!providerId || !parsed || parsed.providerId !== providerId
258
+ || (fields.providerBinding !== undefined && !PROVIDER_BINDING.test(fields.providerBinding))
259
+ || (fields.selectionReason !== undefined && !ATTEMPT_SELECTION_REASONS.has(fields.selectionReason))) {
260
+ fail('IRIS_ATTEMPT_INPUT_INVALID', 'Attempt 的 providerId 与复合 model 必须一致');
261
+ }
262
+ let created;
263
+ mutate(dataRoot, taskId, (task) => {
264
+ if (task.acceptance === 'accepted' || task.acceptance === 'unknown') {
265
+ fail('IRIS_TASK_RESUBMIT_FORBIDDEN', 'Task 已受理或受理未知,禁止创建新 Attempt');
266
+ }
267
+ const previousAttempt = task.attempts[task.attempts.length - 1];
268
+ const queuedAt = previousAttempt?.finishedAt || task.createdAt;
269
+ const submitStartedAt = timestampAfter(queuedAt);
270
+ created = {
271
+ id: 'attempt_' + crypto.randomBytes(12).toString('hex'),
272
+ ordinal: task.attempts.length + 1,
273
+ providerId,
274
+ model,
275
+ stage: 'submitting',
276
+ acceptance: 'none',
277
+ startedAt: submitStartedAt,
278
+ stageTimestamps: { queuedAt, submitStartedAt },
279
+ ...(fields.selectionReason !== undefined ? { selectionReason: fields.selectionReason } : {}),
280
+ ...(fields.providerBinding !== undefined ? { providerBinding: fields.providerBinding } : {})
281
+ };
282
+ task.attempts.push(created);
283
+ task.activeAttemptId = created.id;
284
+ task.providerId = providerId;
285
+ task.modelRef = model;
286
+ if (created.providerBinding) task.providerBinding = created.providerBinding;
287
+ else delete task.providerBinding;
288
+ task.phase = 'submitting';
289
+ task.acceptance = 'none';
290
+ task.watchState = 'idle';
291
+ task.outcome = 'none';
292
+ task.deliveryState = 'none';
293
+ task.cancelState = 'none';
294
+ delete task.remoteTaskId;
295
+ delete task.lastError;
296
+ });
297
+ return Object.freeze(clone(created));
298
+ }
299
+
300
+ export function recordCoreAttemptResult(dataRoot, taskId, snapshot) {
301
+ return mutate(dataRoot, taskId, (task) => {
302
+ const attempt = task.attempts.find((item) => item.id === snapshot?.id);
303
+ if (!attempt) fail('IRIS_ATTEMPT_NOT_FOUND', 'Attempt 不存在');
304
+ if (attempt.acceptance !== 'none') fail('IRIS_ATTEMPT_RESULT_EXISTS', 'Attempt 结果不可重复写入');
305
+ if (attempt.providerId !== snapshot.providerId || attempt.model !== snapshot.model) {
306
+ fail('IRIS_ATTEMPT_IDENTITY_MISMATCH', 'Attempt 结果身份与写前记录不一致');
307
+ }
308
+ const acceptance = snapshot.acceptance;
309
+ if (!['accepted', 'not_accepted', 'unknown'].includes(acceptance)) {
310
+ fail('IRIS_ATTEMPT_RESULT_INVALID', 'Attempt 受理结果无效');
311
+ }
312
+ attempt.acceptance = acceptance;
313
+ attempt.resultKind = String(snapshot.resultKind || '');
314
+ attempt.stage = snapshot.resultKind === 'completed'
315
+ ? 'terminal' : (acceptance === 'accepted' ? 'accepted' : 'terminal');
316
+ const submittedAt = appendAttemptStage(attempt, 'submittedAt') || new Date().toISOString();
317
+ attempt.finishedAt = submittedAt;
318
+ if (snapshot.resultKind === 'completed') appendAttemptStage(attempt, 'remoteCompletedAt');
319
+ if (snapshot.remoteTaskId) attempt.remoteTaskId = String(snapshot.remoteTaskId);
320
+ if (snapshot.error) attempt.error = safeError(snapshot.error, { acceptance });
321
+
322
+ task.acceptance = acceptance;
323
+ task.phase = acceptance === 'accepted' ? 'accepted' : 'terminal';
324
+ task.watchState = 'idle';
325
+ if (acceptance === 'unknown') task.outcome = 'unknown';
326
+ if (acceptance === 'accepted' && snapshot.resultKind === 'completed') {
327
+ task.phase = 'running';
328
+ task.outcome = 'succeeded';
329
+ task.deliveryState = 'pending';
330
+ }
331
+ if (snapshot.remoteTaskId) task.remoteTaskId = String(snapshot.remoteTaskId);
332
+ if (attempt.error) task.lastError = attempt.error;
333
+ else delete task.lastError;
334
+ });
335
+ }
336
+
337
+ export function finalizeCoreNoAcceptance(dataRoot, taskId, error) {
338
+ return mutate(dataRoot, taskId, (task) => {
339
+ if (task.acceptance !== 'not_accepted') {
340
+ fail('IRIS_TASK_ACCEPTANCE_REQUIRED', '只有全部候选明确未受理才能记为提交失败');
341
+ }
342
+ task.phase = 'terminal';
343
+ task.watchState = 'idle';
344
+ task.outcome = 'failed';
345
+ task.lastError = safeError(error, { stage: 'submit', acceptance: 'not_accepted' });
346
+ task.finishedAt = new Date().toISOString();
347
+ });
348
+ }
349
+
350
+ export function activateCoreWatch(dataRoot, taskId) {
351
+ return mutate(dataRoot, taskId, (task) => {
352
+ if (task.acceptance !== 'accepted' || !task.remoteTaskId || !['none', 'unknown'].includes(task.outcome)) {
353
+ fail('IRIS_TASK_NOT_OBSERVABLE', 'Task 缺少可观察的远端受理事实');
354
+ }
355
+ if (task.watchState === 'active') {
356
+ fail('IRIS_TASK_WATCH_ACTIVE', 'Task 已有观察操作正在执行');
357
+ }
358
+ task.phase = 'running';
359
+ task.watchState = 'active';
360
+ task.outcome = 'none';
361
+ delete task.lastError;
362
+ });
363
+ }
364
+
365
+ export function recordCorePollResult(dataRoot, taskId, result) {
366
+ return mutate(dataRoot, taskId, (task) => {
367
+ if (task.acceptance !== 'accepted' || !task.remoteTaskId) {
368
+ fail('IRIS_TASK_NOT_OBSERVABLE', 'Task 缺少可观察的远端受理事实');
369
+ }
370
+ if (result.kind === 'pending') {
371
+ task.phase = 'running';
372
+ task.watchState = 'suspended';
373
+ task.outcome = 'none';
374
+ if (result.progress) task.progress = String(result.progress);
375
+ return;
376
+ }
377
+ if (['succeeded', 'failed', 'canceled'].includes(result.kind)) {
378
+ appendAttemptStage(activeAttempt(task), 'remoteCompletedAt');
379
+ }
380
+ task.phase = 'terminal';
381
+ task.watchState = result.kind === 'unknown' ? 'exhausted' : 'idle';
382
+ if (result.kind === 'succeeded') {
383
+ task.phase = 'running';
384
+ task.outcome = 'succeeded';
385
+ task.deliveryState = 'pending';
386
+ delete task.lastError;
387
+ return;
388
+ }
389
+ if (result.kind === 'canceled') {
390
+ task.outcome = 'canceled';
391
+ task.cancelState = 'remote_confirmed';
392
+ } else if (result.kind === 'failed') task.outcome = 'failed';
393
+ else task.outcome = 'unknown';
394
+ if (result.error) task.lastError = safeError(result.error, { stage: 'poll', acceptance: 'accepted' });
395
+ task.finishedAt = new Date().toISOString();
396
+ });
397
+ }
398
+
399
+ export function recordCoreWatchError(dataRoot, taskId, error) {
400
+ return mutate(dataRoot, taskId, (task) => {
401
+ if (task.acceptance !== 'accepted') fail('IRIS_TASK_NOT_OBSERVABLE', 'Task 尚未被远端受理');
402
+ task.phase = 'running';
403
+ task.watchState = 'suspended';
404
+ task.outcome = 'unknown';
405
+ task.lastError = safeError(error, { stage: 'poll', acceptance: 'accepted', retryable: true });
406
+ });
407
+ }
408
+
409
+ export function beginCoreDelivery(dataRoot, taskId) {
410
+ return mutate(dataRoot, taskId, (task) => {
411
+ if (task.outcome !== 'succeeded' || task.deliveryState !== 'pending') {
412
+ fail('IRIS_TASK_NOT_DELIVERABLE', 'Task 不处于待交付状态');
413
+ }
414
+ task.phase = 'running';
415
+ task.watchState = 'idle';
416
+ task.deliveryState = 'downloading';
417
+ const attempt = activeAttempt(task);
418
+ if (attempt?.stageTimestamps) {
419
+ delete attempt.stageTimestamps.downloadStartedAt;
420
+ delete attempt.stageTimestamps.downloadedAt;
421
+ delete attempt.stageTimestamps.localProcessedAt;
422
+ appendAttemptStage(attempt, 'downloadStartedAt');
423
+ }
424
+ });
425
+ }
426
+
427
+ /** 全部远端字节已经落入并校验 staging;Artifact 提交尚未开始。 */
428
+ export function recordCoreDownloadCompleted(dataRoot, taskId) {
429
+ return mutate(dataRoot, taskId, (task) => {
430
+ if (task.outcome !== 'succeeded' || task.deliveryState !== 'downloading') {
431
+ fail('IRIS_TASK_NOT_DELIVERABLE', 'Task 交付状态无效');
432
+ }
433
+ appendAttemptStage(activeAttempt(task), 'downloadedAt');
434
+ });
435
+ }
436
+
437
+ export function prepareCoreRedelivery(dataRoot, taskId) {
438
+ return mutate(dataRoot, taskId, (task) => {
439
+ if (task.outcome !== 'succeeded' || task.deliveryState !== 'failed' || !task.remoteTaskId) {
440
+ fail('IRIS_TASK_NOT_REDELIVERABLE', 'Task 不处于可重新交付状态');
441
+ }
442
+ task.phase = 'running';
443
+ task.deliveryState = 'pending';
444
+ delete task.lastError;
445
+ });
446
+ }
447
+
448
+ export function completeCoreDelivery(dataRoot, taskId, artifactIds) {
449
+ return mutate(dataRoot, taskId, (task) => {
450
+ if (task.outcome !== 'succeeded' || task.deliveryState !== 'downloading') {
451
+ fail('IRIS_TASK_NOT_DELIVERABLE', 'Task 交付状态无效');
452
+ }
453
+ task.phase = 'terminal';
454
+ task.watchState = 'idle';
455
+ task.deliveryState = 'ready';
456
+ task.artifactIds = [...new Set((artifactIds || []).map(String))];
457
+ const attempt = activeAttempt(task);
458
+ appendAttemptStage(attempt, 'downloadedAt');
459
+ task.finishedAt = appendAttemptStage(attempt, 'localProcessedAt')
460
+ || new Date().toISOString();
461
+ delete task.lastError;
462
+ });
463
+ }
464
+
465
+ export function failCoreDelivery(dataRoot, taskId, error) {
466
+ return mutate(dataRoot, taskId, (task) => {
467
+ if (task.outcome !== 'succeeded') fail('IRIS_TASK_NOT_DELIVERABLE', '只有生成成功的 Task 能记录交付失败');
468
+ task.phase = 'terminal';
469
+ task.watchState = 'idle';
470
+ task.deliveryState = 'failed';
471
+ task.lastError = safeError(error, { stage: 'download', category: 'local_io', acceptance: 'accepted' });
472
+ task.finishedAt = new Date().toISOString();
473
+ });
474
+ }
475
+
476
+ export function requestCoreCancel(dataRoot, taskId) {
477
+ return mutate(dataRoot, taskId, (task) => {
478
+ if (task.acceptance !== 'accepted' || !task.remoteTaskId || !['none', 'unknown'].includes(task.outcome)) {
479
+ fail('IRIS_TASK_NOT_CANCELABLE', 'Task 缺少可取消的远端受理事实');
480
+ }
481
+ task.cancelState = 'requested';
482
+ task.watchState = 'suspended';
483
+ });
484
+ }
485
+
486
+ export function recordCoreCancelResult(dataRoot, taskId, result) {
487
+ return mutate(dataRoot, taskId, (task) => {
488
+ if (task.cancelState !== 'requested') fail('IRIS_TASK_CANCEL_NOT_REQUESTED', 'Task 尚未请求取消');
489
+ // 远端事实优先:竞态下 poll/交付已沉淀终态结果,取消结果不得覆盖它。
490
+ if (['succeeded', 'failed', 'canceled'].includes(task.outcome)) {
491
+ task.cancelState = task.outcome === 'canceled' ? 'remote_confirmed' : 'none';
492
+ return;
493
+ }
494
+ if (result.kind === 'canceled') {
495
+ task.phase = 'terminal';
496
+ task.watchState = 'idle';
497
+ task.outcome = 'canceled';
498
+ task.cancelState = 'remote_confirmed';
499
+ task.finishedAt = new Date().toISOString();
500
+ return;
501
+ }
502
+ if (result.kind === 'not_supported') {
503
+ // Provider 明确不支持远端取消:确认取消未发生,回到可继续观察的真实状态;绝不伪造 canceled。
504
+ task.cancelState = 'none';
505
+ task.watchState = 'suspended';
506
+ if (result.error) task.lastError = safeError(result.error, { stage: 'cancel', acceptance: 'accepted' });
507
+ return;
508
+ }
509
+ // unknown/网络或超时失败:无法确认远端;保持非终态与真实 outcome,显式 reobserve 可收敛。
510
+ task.outcome = 'unknown';
511
+ task.cancelState = 'unknown';
512
+ task.watchState = 'suspended';
513
+ if (result.error) task.lastError = safeError(result.error, { stage: 'cancel', acceptance: 'accepted' });
514
+ });
515
+ }
516
+
517
+ /**
518
+ * 进程在 submit 返回落盘前退出时,没有 remoteTaskId 可以安全观察;恢复只能把
519
+ * 事实收口为受理未知。已经落盘 remoteTaskId 的任务保持可观察,绝不新建 Attempt。
520
+ */
521
+ export function recoverCoreTask(dataRoot, taskId) {
522
+ const current = readRecord(dataRoot, taskId);
523
+ if (current.cancelState === 'requested') {
524
+ return mutate(dataRoot, taskId, (task) => {
525
+ task.phase = 'terminal';
526
+ task.watchState = 'suspended';
527
+ task.outcome = 'unknown';
528
+ task.cancelState = 'unknown';
529
+ task.lastError = safeError('取消响应在落盘前中断', {
530
+ stage: 'cancel', category: 'unknown', acceptance: 'accepted'
531
+ });
532
+ });
533
+ }
534
+ if (current.outcome === 'succeeded' && ['pending', 'downloading'].includes(current.deliveryState)) {
535
+ return mutate(dataRoot, taskId, (task) => {
536
+ task.phase = 'terminal';
537
+ task.watchState = 'idle';
538
+ task.deliveryState = 'failed';
539
+ task.lastError = safeError('产物交付在完成落盘前中断', {
540
+ stage: 'download', category: 'local_io', acceptance: 'accepted'
541
+ });
542
+ task.finishedAt = new Date().toISOString();
543
+ });
544
+ }
545
+ if (current.acceptance === 'accepted' && current.watchState === 'active') {
546
+ return mutate(dataRoot, taskId, (task) => { task.watchState = 'suspended'; });
547
+ }
548
+ if (current.phase !== 'submitting' || current.acceptance !== 'none') return publicTask(current);
549
+ return mutate(dataRoot, taskId, (task) => {
550
+ const attempt = task.attempts.find((item) => item.id === task.activeAttemptId);
551
+ if (attempt && attempt.acceptance === 'none') {
552
+ attempt.acceptance = 'unknown';
553
+ attempt.resultKind = 'acceptance_unknown';
554
+ attempt.stage = 'terminal';
555
+ attempt.finishedAt = new Date().toISOString();
556
+ attempt.error = safeError('提交进程在受理结果落盘前中断', {
557
+ stage: 'response', category: 'unknown', acceptance: 'unknown'
558
+ });
559
+ task.lastError = attempt.error;
560
+ }
561
+ task.phase = 'terminal';
562
+ task.acceptance = 'unknown';
563
+ task.watchState = 'idle';
564
+ task.outcome = 'unknown';
565
+ task.finishedAt = new Date().toISOString();
566
+ });
567
+ }