zigmini 0.8.0-beta.2 → 0.8.0-beta.3
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/assets/agent/gateway-rules.md +1 -0
- package/assets/android/zigmini.apk +0 -0
- package/assets/android/zigmini.apk.json +3 -3
- package/assets/build.json +1 -1
- package/host/omo-bridge/task-control.ts +7 -4
- package/package.json +2 -2
- package/src/client-topics.ts +16 -0
- package/src/decisions.ts +22 -5
- package/src/hub.ts +3 -1
- package/src/main.ts +9 -4
- package/src/store.ts +15 -4
- package/src/work.ts +6 -2
|
@@ -17,3 +17,4 @@ zigmini 패키지 공통 에이전트 규칙입니다. 게이트웨이 브리지
|
|
|
17
17
|
12. 로컬 파일 경로를 웹 링크처럼 보여 주지 않습니다.
|
|
18
18
|
13. 사용자의 시간 표현은 사용자의 현지 시간으로 해석합니다. 보고하는 시각도 현지 시간으로 씁니다.
|
|
19
19
|
14. 사용자는 게이트웨이 프로필의 닉네임으로 부릅니다.
|
|
20
|
+
15. main은 오래 기다리거나 오래 실행하는 작업(배포, 전체 테스트, 뮤테이션 테스트, 다른 작업 기다리기)을 맡기면, 그 작업의 세션 기록이 30분 넘게 바뀌지 않을 때 main을 깨우는 감시를 직접 겁니다. 작업이 끝나면 감시를 끕니다. 작업의 "기다리는 중"·"진행 중" 보고만 믿지 않습니다. 사용자에게 그렇게 말하기 전에 그 작업의 마지막 기록 시각과 실행 중인 프로세스를 확인합니다. 작업이 멈췄으면 바로 그 작업에 조치를 보내고 사용자에게 알립니다. 작업은 긴 명령을 세션과 분리해 실행하고, 결과 파일을 직접 확인하며, 시간 상한을 겁니다.
|
|
Binary file
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"versionCode":
|
|
3
|
-
"versionName": "0.8.0-beta.
|
|
4
|
-
"sha256": "
|
|
2
|
+
"versionCode": 22,
|
|
3
|
+
"versionName": "0.8.0-beta.3",
|
|
4
|
+
"sha256": "819f50b65105dde875324e7dde62635321ee1412ba1e975849a764f10dea3426"
|
|
5
5
|
}
|
package/assets/build.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"commit":"
|
|
1
|
+
{"commit":"d6641bcbb5a3d30fe55d94c24f05113f3405518b"}
|
|
@@ -47,8 +47,11 @@ function commandEntry(entries: Entry[], workId: string, seq: unknown): Params {
|
|
|
47
47
|
throw new Error("report_command_not_received");
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
/**
|
|
51
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Main and work models have sent report details as a JSON string (bugs 3 and 127); only a string that parses to an
|
|
52
|
+
* object is unwrapped.
|
|
53
|
+
*/
|
|
54
|
+
function reportDetails(details: unknown): unknown {
|
|
52
55
|
if (typeof details !== "string") return details;
|
|
53
56
|
try {
|
|
54
57
|
const parsed: unknown = JSON.parse(details);
|
|
@@ -204,7 +207,7 @@ export default function taskControl(pi: ExtensionApi): void {
|
|
|
204
207
|
Object.keys(params).some((key) => !["kind", "summary", "details"].includes(key)))) {
|
|
205
208
|
throw new Error("main_question_only");
|
|
206
209
|
}
|
|
207
|
-
const details =
|
|
210
|
+
const details = reportDetails(params.details);
|
|
208
211
|
const command = isMain
|
|
209
212
|
? mainDecisionEntry(ctx.sessionManager.getEntries(), details)
|
|
210
213
|
: commandEntry(ctx.sessionManager.getEntries(), workId as string, params.command_seq);
|
|
@@ -214,7 +217,7 @@ export default function taskControl(pi: ExtensionApi): void {
|
|
|
214
217
|
kind: "question", summary: params.summary, details,
|
|
215
218
|
} : {
|
|
216
219
|
report_id: requestKey, work_id: workId, command_id: command.command_id,
|
|
217
|
-
kind: params.kind, command_seq: params.command_seq, summary: params.summary, details
|
|
220
|
+
kind: params.kind, command_seq: params.command_seq, summary: params.summary, details,
|
|
218
221
|
};
|
|
219
222
|
let entry = ctx.sessionManager.getEntries().find((e) => e.type === "custom" && e.customType === "zgw.work_report" && record(e.data) && e.data.report_id === requestKey);
|
|
220
223
|
if (entry && !isDeepStrictEqual(entry.data, data)) throw new Error("report_key_conflict");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zigmini",
|
|
3
|
-
"version": "0.8.0-beta.
|
|
3
|
+
"version": "0.8.0-beta.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"zigmini": "bin/zigmini"
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"test:e2e": "bun test test/e2e",
|
|
54
54
|
"test:contract": "bun test test/contract",
|
|
55
55
|
"test:all": "tsc --noEmit && bun test test/unit && bun test test/contract",
|
|
56
|
-
"test:mutation": "bun
|
|
56
|
+
"test:mutation": "bun scripts/mutation-run.ts",
|
|
57
57
|
"heavy": "bun host/heavy-slot.ts run"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bug 119: which chat socket gets which live event. Every chat socket joins both topics; the app (hello client "android",
|
|
3
|
+
* the same in every app version) leaves the thinking topic, since it never shows thinking and each delta costs it work on
|
|
4
|
+
* its main thread. The web page and devlog show thinking and stay on it.
|
|
5
|
+
*/
|
|
6
|
+
export const CHAT_TOPIC = "clients";
|
|
7
|
+
export const THINKING_TOPIC = "thinking";
|
|
8
|
+
export const APP_CLIENT = "android";
|
|
9
|
+
|
|
10
|
+
export function topicOf(type: string): string {
|
|
11
|
+
return type === "thinking.delta" ? THINKING_TOPIC : CHAT_TOPIC;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function showsThinking(client: unknown): boolean {
|
|
15
|
+
return client !== APP_CLIENT;
|
|
16
|
+
}
|
package/src/decisions.ts
CHANGED
|
@@ -44,6 +44,16 @@ function unresolved(decision: Decision): boolean {
|
|
|
44
44
|
return decision.status !== "closed" && decision.status !== "delivered";
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Bug 127: an unanswered ask_user_question with waitForAnswer false. Its tool call already returned, so its owner never
|
|
49
|
+
* waits on it; when its native wait ends (a generation drain, an owner release) the answer still reaches the owner as a
|
|
50
|
+
* follow-up command.
|
|
51
|
+
*/
|
|
52
|
+
function detachedQuestion(decision: Decision): boolean {
|
|
53
|
+
return decision.method === "question" && !decision.permission && decision.submissionId === null &&
|
|
54
|
+
record(decision.payload) && decision.payload.waitForAnswer === false;
|
|
55
|
+
}
|
|
56
|
+
|
|
47
57
|
/** Chat card footer for an item the user deleted from the inbox (web markResolved, Android resolvedLabel). */
|
|
48
58
|
const DISMISSED_REASONS: Record<string, string> = {
|
|
49
59
|
user_cancelled: "확인함에서 삭제해 질문을 취소했습니다",
|
|
@@ -304,6 +314,8 @@ export class DecisionService {
|
|
|
304
314
|
...observation.payload, ...(workId === null ? { sourceEventSeq } : {}),
|
|
305
315
|
...(Array.isArray(sourceReferences) && sourceReferences.length ? { sourceReferences } : {}),
|
|
306
316
|
};
|
|
317
|
+
// A decision stored before bug 127 has no waitForAnswer mark; observing it again must not read as a changed question.
|
|
318
|
+
if (decision && record(decision.payload) && !Object.hasOwn(decision.payload, "waitForAnswer")) delete payload.waitForAnswer;
|
|
307
319
|
const bindingState = observation.permission && record(payload.nativePermission) &&
|
|
308
320
|
!this.store.hasNativePermissionAsked(owner.scope, owner.sessionPath, payload.nativePermission) ? "unknown" : "live";
|
|
309
321
|
if (!decision) {
|
|
@@ -358,7 +370,8 @@ export class DecisionService {
|
|
|
358
370
|
if (observation.method !== "question" || observation.permission || observation.required === false || typeof toolCallId !== "string" || !toolCallId) return null;
|
|
359
371
|
const siblings = this.store.allDecisions(owner.scope).filter((decision) => decision.sourceSessionPath === owner.sessionPath &&
|
|
360
372
|
decision.method === "question" && !decision.permission && record(decision.payload) && decision.payload.toolCallId === toolCallId && unresolved(decision));
|
|
361
|
-
const waiting = siblings.filter((decision) => decision.required && decision.bindingState === "unknown" && decision.reason === "owner_disconnected"
|
|
373
|
+
const waiting = siblings.filter((decision) => ((decision.required && decision.bindingState === "unknown" && decision.reason === "owner_disconnected") ||
|
|
374
|
+
decision.reason === "async_wait_closed") &&
|
|
362
375
|
decision.submissionId === null && decision.answer === null && record(decision.payload) &&
|
|
363
376
|
isDeepStrictEqual(decision.payload.questions, observation.payload.questions));
|
|
364
377
|
if (siblings.length !== 1 || waiting.length !== 1) return null;
|
|
@@ -413,15 +426,17 @@ export class DecisionService {
|
|
|
413
426
|
decision.submissionId !== null && record(answer) && record(answer.answers) && receiptOutcome &&
|
|
414
427
|
isDeepStrictEqual(event.answers, answer.answers) && (event.comment ?? "") === (answer.comment ?? "") &&
|
|
415
428
|
Array.isArray(event.unanswered) && event.unanswered.length === 0;
|
|
429
|
+
const detached = !received && event.outcome !== "timed_out" && detachedQuestion(decision);
|
|
416
430
|
decision = this.store.setDecisionBinding(decision.id, decision.revision, {
|
|
417
431
|
state: event.outcome === "timed_out" ? "expired" : "closed",
|
|
418
|
-
reason: event.outcome === "timed_out" ? "timeout" : "engine_wait_closed",
|
|
432
|
+
reason: event.outcome === "timed_out" ? "timeout" : detached ? "async_wait_closed" : "engine_wait_closed",
|
|
419
433
|
});
|
|
420
434
|
if (received) {
|
|
421
435
|
this.store.transitionDecision(decision.id, decision.revision, { status: "delivered", reason: "question_receipt" });
|
|
422
436
|
} else if (decision.submissionId !== null) {
|
|
423
437
|
this.review(decision.id, record(answer) && answer.cancelled === true ? "user_deferred" : "receipt_unknown");
|
|
424
|
-
} else if (event.outcome !== "timed_out") {
|
|
438
|
+
} else if (event.outcome !== "timed_out" && !detached) {
|
|
439
|
+
// A detached question stays pending: its answer goes by resume, as a follow-up command.
|
|
425
440
|
this.review(decision.id, "engine_wait_closed");
|
|
426
441
|
}
|
|
427
442
|
this.refresh();
|
|
@@ -446,7 +461,8 @@ export class DecisionService {
|
|
|
446
461
|
for (const decision of this.store.allDecisions(owner.scope)) {
|
|
447
462
|
if (decision.sourceSessionPath !== owner.sessionPath || decision.method !== "question" ||
|
|
448
463
|
decision.bindingState !== "live" || !unresolved(decision) || ids.has(decision.sourceRequestId)) continue;
|
|
449
|
-
this.store.setDecisionBinding(decision.id, decision.revision,
|
|
464
|
+
this.store.setDecisionBinding(decision.id, decision.revision, detachedQuestion(decision)
|
|
465
|
+
? { state: "closed", reason: "async_wait_closed" } : { state: "unknown", reason: "request_missing" });
|
|
450
466
|
}
|
|
451
467
|
this.refresh();
|
|
452
468
|
}
|
|
@@ -468,7 +484,8 @@ export class DecisionService {
|
|
|
468
484
|
if (this.stopped) return;
|
|
469
485
|
for (const decision of this.store.allDecisions(owner.scope)) {
|
|
470
486
|
if (decision.sourceSessionPath !== owner.sessionPath || decision.bindingState !== "live" || !unresolved(decision)) continue;
|
|
471
|
-
this.store.setDecisionBinding(decision.id, decision.revision,
|
|
487
|
+
this.store.setDecisionBinding(decision.id, decision.revision, detachedQuestion(decision)
|
|
488
|
+
? { state: "closed", reason: "async_wait_closed" } : { state: "unknown", reason: "owner_disconnected" });
|
|
472
489
|
}
|
|
473
490
|
this.refresh();
|
|
474
491
|
}
|
package/src/hub.ts
CHANGED
|
@@ -186,6 +186,7 @@ interface QuestionItem {
|
|
|
186
186
|
export interface ScopedQuestionFrame {
|
|
187
187
|
id: string;
|
|
188
188
|
toolCallId?: string;
|
|
189
|
+
waitForAnswer?: boolean;
|
|
189
190
|
questions: QuestionItem[];
|
|
190
191
|
askedAtMs?: number;
|
|
191
192
|
deadlineAtMs?: number;
|
|
@@ -1786,7 +1787,8 @@ export class Hub {
|
|
|
1786
1787
|
const decision = this.decisions?.observe(owner, {
|
|
1787
1788
|
actionId, sourceRequestId: frame.id, method: "question", permission: false,
|
|
1788
1789
|
payload: { title: card.title, detail: card.detail, options: [], questions: frame.questions,
|
|
1789
|
-
...(frame.toolCallId ? { toolCallId: frame.toolCallId } : {})
|
|
1790
|
+
...(frame.toolCallId ? { toolCallId: frame.toolCallId } : {}),
|
|
1791
|
+
...(frame.waitForAnswer === false ? { waitForAnswer: false } : {}) },
|
|
1790
1792
|
deadlineAt: frame.deadlineAtMs ?? null,
|
|
1791
1793
|
});
|
|
1792
1794
|
const open = this.openQuestionCards(owner.conv, owner.scope);
|
package/src/main.ts
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
SYSTEM_NETWORK,
|
|
30
30
|
} from "./enroll.ts";
|
|
31
31
|
import { ACCESS_PATH, handleAccessApi, pingAccess, systemTailscale } from "./cli/access.ts";
|
|
32
|
+
import { CHAT_TOPIC, showsThinking, THINKING_TOPIC, topicOf } from "./client-topics.ts";
|
|
32
33
|
import { APK_DIR, APK_PATH, APP_DOWNLOAD_LINK_PATH, APP_VERSION_PATH, ApkSource, appVersion, createDownloadLink, DownloadLinks, serveApk } from "./app-download.ts";
|
|
33
34
|
import { loadConfig } from "./config.ts";
|
|
34
35
|
import { memoUpdatedData, parseMemoPatch } from "./memo-images.ts";
|
|
@@ -142,7 +143,6 @@ const updateApi: UpdateApiDeps = {
|
|
|
142
143
|
now: () => new Date(),
|
|
143
144
|
};
|
|
144
145
|
|
|
145
|
-
const TOPIC = "clients";
|
|
146
146
|
let publish: (envelope: Envelope) => void = () => {};
|
|
147
147
|
const hub = new Hub(
|
|
148
148
|
store,
|
|
@@ -397,6 +397,7 @@ async function handleClient(ws: ServerWebSocket<Conn>, raw: string): Promise<voi
|
|
|
397
397
|
// question cannot race into a phone notification between those adjacent frames.
|
|
398
398
|
questionNotifications.setWebPresence(ws, true);
|
|
399
399
|
}
|
|
400
|
+
if (!showsThinking(msg.data?.client)) ws.unsubscribe(THINKING_TOPIC);
|
|
400
401
|
const state = hub.snapshot();
|
|
401
402
|
const snapshot = { ...state, approvals: omoCheckNotifications.filterApprovals(state.approvals) };
|
|
402
403
|
// While omo is reconnecting, Hub intentionally clears its in-memory question cards until
|
|
@@ -806,7 +807,10 @@ async function routeApi(req: Request, url: URL): Promise<Response> {
|
|
|
806
807
|
|
|
807
808
|
const websocket: WebSocketHandler<Conn> = {
|
|
808
809
|
open(ws) {
|
|
809
|
-
if (!ws.data.notifyOnly)
|
|
810
|
+
if (!ws.data.notifyOnly) {
|
|
811
|
+
ws.subscribe(CHAT_TOPIC);
|
|
812
|
+
ws.subscribe(THINKING_TOPIC);
|
|
813
|
+
}
|
|
810
814
|
if (ws.data.trusted && !ws.data.notifyOnly) ws.subscribe(TRUSTED_TOPIC);
|
|
811
815
|
const id = ws.data.deviceId;
|
|
812
816
|
if (id) {
|
|
@@ -837,8 +841,9 @@ publish = (e) => {
|
|
|
837
841
|
if (!omoCheckNotifications.allow(e)) return;
|
|
838
842
|
questionNotifications.onEnvelope(e);
|
|
839
843
|
const text = JSON.stringify(e);
|
|
840
|
-
|
|
841
|
-
|
|
844
|
+
const topic = topicOf(e.type);
|
|
845
|
+
trustedServer.publish(topic, text);
|
|
846
|
+
lanServer?.publish(topic, text);
|
|
842
847
|
// A decision change can change a work's 입력 대기 state (its open decisions, or its status after a delivered answer).
|
|
843
848
|
if (e.type === "activity.updated" || e.type === "conversation.created" || e.type === "decision.updated") workStatus.schedule();
|
|
844
849
|
};
|
package/src/store.ts
CHANGED
|
@@ -215,7 +215,7 @@ export interface WorkStartRequest extends WorkCommandRequest {
|
|
|
215
215
|
export type WorkReport = Pick<WorkUpdate, "sourceKey" | "kind" | "commandSeq" | "summary" | "details">;
|
|
216
216
|
|
|
217
217
|
export class WorkStoreError extends Error {
|
|
218
|
-
constructor(readonly code: "work_not_found" | "command_not_found" | "request_key_conflict" | "report_conflict" | "invalid_report_command" | "invalid_delivery_transition" | "invalid_publish" | "publish_conflict" | "unresolved_work_commands" | "required_decisions_pending" | "question_without_decision", readonly ids: readonly string[] = []) {
|
|
218
|
+
constructor(readonly code: "work_not_found" | "command_not_found" | "request_key_conflict" | "report_conflict" | "invalid_report_command" | "invalid_delivery_transition" | "invalid_publish" | "publish_conflict" | "unresolved_work_commands" | "required_decisions_pending" | "question_without_decision" | "invalid_replacement_details", readonly ids: readonly string[] = []) {
|
|
219
219
|
super(code);
|
|
220
220
|
}
|
|
221
221
|
}
|
|
@@ -420,8 +420,12 @@ export function replacementQuestionPayload(payload: Record<string, unknown>): Re
|
|
|
420
420
|
options: Array.isArray(withIds.options) ? withIds.options : [] };
|
|
421
421
|
}
|
|
422
422
|
|
|
423
|
-
/**
|
|
424
|
-
|
|
423
|
+
/**
|
|
424
|
+
* Closed bindings whose question still takes an answer, delivered to the owner as a follow-up command. Bug 127:
|
|
425
|
+
* async_wait_closed (a waitForAnswer false question whose native wait ended) and recheck_incomplete (a recheck turn
|
|
426
|
+
* ended without a replacement; the question stands as asked). A permission is never resumed (submitDecision, info).
|
|
427
|
+
*/
|
|
428
|
+
export const RESUMABLE_CLOSED: ReadonlySet<string> = new Set(["report_only", "main_released", "async_wait_closed", "recheck_incomplete"]);
|
|
425
429
|
|
|
426
430
|
/** Checks structural completeness, not the semantic appropriateness of a historical answer. */
|
|
427
431
|
function completeDecisionAnswer(method: DecisionMethod, payload: unknown, response: unknown): boolean {
|
|
@@ -1374,7 +1378,14 @@ export class Store {
|
|
|
1374
1378
|
if (command && report.kind !== "observation" && (command.delivery === "pending" || command.delivery === "sending" || command.delivery === "unknown")) {
|
|
1375
1379
|
this.setWorkCommandDelivery(command.id, "accepted", { disposition: "work_report" });
|
|
1376
1380
|
}
|
|
1377
|
-
|
|
1381
|
+
// Bug 127: a question report on a recheck command must carry its replacement as an object; anything else would be
|
|
1382
|
+
// accepted without replacing and leave the old question recheck-only.
|
|
1383
|
+
const replacing = object(report.details) && typeof report.details.replaces_decision_id === "string";
|
|
1384
|
+
if (command && report.kind === "question" && !replacing && object(command.payload) &&
|
|
1385
|
+
(command.payload.decision_operation === "recheck" || command.payload.decision_operation === "request_permission")) {
|
|
1386
|
+
throw new WorkStoreError("invalid_replacement_details", [workId]);
|
|
1387
|
+
}
|
|
1388
|
+
if (command && report.kind === "question" && replacing) {
|
|
1378
1389
|
this.replaceDecisionFromReport({
|
|
1379
1390
|
scope: `work:${workId}`, sourceSessionPath: work.sessionPath,
|
|
1380
1391
|
reportId: report.sourceKey, commandId: command.id, details: report.details,
|
package/src/work.ts
CHANGED
|
@@ -85,6 +85,8 @@ const REPORT_RULE = "Report completed or failed with the highest command_seq of
|
|
|
85
85
|
const QUESTION_WITHOUT_DECISION = "No inbox question is open for this work. Ask the user with ask_user_question first " +
|
|
86
86
|
"(waitForAnswer true for a required confirmation, which then needs no kind=question report; " +
|
|
87
87
|
"waitForAnswer false only if the work may continue without the answer and you also report), then send kind=question again. A kind=question report alone creates no inbox question.";
|
|
88
|
+
const INVALID_REPLACEMENT_DETAILS = "This report answers a recheck, so details must be a JSON object, not a string: " +
|
|
89
|
+
"{replaces_decision_id, submission_id, and replacement:{method,payload,required} or replacement_decision_id}. Send the report again with details as an object.";
|
|
88
90
|
function envelope(command: WorkCommand): string {
|
|
89
91
|
return PREFIX + JSON.stringify({ version: 1, work_id: command.workId, command_id: command.id, command_seq: command.seq, kind: command.kind, payload: command.payload, report_rule: REPORT_RULE });
|
|
90
92
|
}
|
|
@@ -155,7 +157,9 @@ export class WorkRouter {
|
|
|
155
157
|
} catch (error) {
|
|
156
158
|
if (error instanceof RoutingError) return Response.json({ error: error.code }, { status: error.status });
|
|
157
159
|
if (error instanceof WorkStoreError) return Response.json({ error: error.code, ids: error.ids,
|
|
158
|
-
...(error.code === "question_without_decision" ? { detail: QUESTION_WITHOUT_DECISION } : {})
|
|
160
|
+
...(error.code === "question_without_decision" ? { detail: QUESTION_WITHOUT_DECISION } : {}),
|
|
161
|
+
...(error.code === "invalid_replacement_details" ? { detail: INVALID_REPLACEMENT_DETAILS } : {}) },
|
|
162
|
+
{ status: error.code === "invalid_replacement_details" ? 400 : 409 });
|
|
159
163
|
if (error instanceof DecisionStoreError) return Response.json({ error: error.code }, {
|
|
160
164
|
status: error.code === "decision_not_found" ? 404 : error.code.startsWith("invalid_") ? 400 : 409,
|
|
161
165
|
});
|
|
@@ -601,7 +605,7 @@ export class WorkRouter {
|
|
|
601
605
|
} catch (error) {
|
|
602
606
|
// The live report call already showed the worker this refusal; recovery skips the entry.
|
|
603
607
|
// Unresolved commands retain their receipt state; completion needs a new valid report.
|
|
604
|
-
if (error instanceof WorkStoreError && ["question_without_decision", "unresolved_work_commands"].includes(error.code)) continue;
|
|
608
|
+
if (error instanceof WorkStoreError && ["question_without_decision", "unresolved_work_commands", "invalid_replacement_details"].includes(error.code)) continue;
|
|
605
609
|
if (!(error instanceof WorkStoreError) || error.code !== "required_decisions_pending") throw error;
|
|
606
610
|
deferred = Math.min(deferred, index);
|
|
607
611
|
}
|