fix(agent): bound SCADA presentation continuations
This commit is contained in:
@@ -38,6 +38,8 @@ Task chart windows are managed by the browser in one parent window containing pr
|
|||||||
|
|
||||||
Task open_chart requires persisted evidence references. Task set_overlay can omit data; Server resolves immutable evidence into bounded GeoJSON. Supplied data must match that resolution and never overrides the stored snapshot. Chat and Dev supply resolved data for structural validation before rendering.
|
Task open_chart requires persisted evidence references. Task set_overlay can omit data; Server resolves immutable evidence into bounded GeoJSON. Supplied data must match that resolution and never overrides the stored snapshot. Chat and Dev supply resolved data for structural validation before rendering.
|
||||||
|
|
||||||
|
If Server rejects completion with `SCADA_PRESENTATION_INCOMPLETE`, the host may resume the same investigation up to three times within the current response to publish the missing charts. Further rejection follows the existing explicit run-failure path; heartbeats must not keep an endless sequence of extra model turns alive. Permission, transport and other completion failures do not trigger this continuation.
|
||||||
|
|
||||||
Same-time-of-day current/history comparisons use `chart_type=time_comparison`; ordinary charts retain `line` or `bar`. The comparison axis contains shared UTC+8 `HH:mm` or `HH:mm:ss` slots, with current `line` and historical `scatter` series and aligned real `sample_times`. The Server automatically validates new task presentation and report chart publications, rejecting date-appended axes, missing timestamps, misplaced points and nonhistorical reference dates with `TIME_COMPARISON_INVALID`. Chat open_chart is checked by both the native tool and host coordinator using the same generated chart refinement as task publication; Dev commands and ui_block.Chart use it too. Ordinary line/bar charts skip comparison semantics, but a line/scatter comparison cannot be labelled line. Historical stored chart read contracts remain unchanged.
|
Same-time-of-day current/history comparisons use `chart_type=time_comparison`; ordinary charts retain `line` or `bar`. The comparison axis contains shared UTC+8 `HH:mm` or `HH:mm:ss` slots, with current `line` and historical `scatter` series and aligned real `sample_times`. The Server automatically validates new task presentation and report chart publications, rejecting date-appended axes, missing timestamps, misplaced points and nonhistorical reference dates with `TIME_COMPARISON_INVALID`. Chat open_chart is checked by both the native tool and host coordinator using the same generated chart refinement as task publication; Dev commands and ui_block.Chart use it too. Ordinary line/bar charts skip comparison semantics, but a line/scatter comparison cannot be labelled line. Historical stored chart read contracts remain unchanged.
|
||||||
|
|
||||||
Chat, task following and Dev previews share the frontend command dispatcher but own independent map sources, SCADA layers and chart state. Clearing or switching one owner does not clear another. Dev JSONL is an explicit local preview/import/export container, not an alternative online business protocol. Plain data records and ui_block records do not execute embedded actions. The earlier agent-ui@1 registry has no active tool mappings and is not an action entry point.
|
Chat, task following and Dev previews share the frontend command dispatcher but own independent map sources, SCADA layers and chart state. Clearing or switching one owner does not clear another. Dev JSONL is an explicit local preview/import/export container, not an alternative online business protocol. Plain data records and ui_block records do not execute embedded actions. The earlier agent-ui@1 registry has no active tool mappings and is not an action entry point.
|
||||||
|
|||||||
@@ -410,6 +410,17 @@ export function scadaPresentationContinuation(error: unknown): string | null {
|
|||||||
return `Server尚未接受本轮调查结束:${error.message}\n请按返回设备补充publish_task_presentation监测图,必要时更新publish_task_report。保留目标设备与真实缺测说明,完成后再结束本轮。`;
|
return `Server尚未接受本轮调查结束:${error.message}\n请按返回设备补充publish_task_presentation监测图,必要时更新publish_task_report。保留目标设备与真实缺测说明,完成后再结束本轮。`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Bound automatic extra model turns to the current response, preserving the Server gate. */
|
||||||
|
export function createScadaPresentationContinuation() {
|
||||||
|
let attempts = 0;
|
||||||
|
return (error: unknown): string | null => {
|
||||||
|
const guidance = scadaPresentationContinuation(error);
|
||||||
|
if (!guidance || attempts >= 3) return null;
|
||||||
|
attempts++;
|
||||||
|
return guidance;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const parseJson = (text: string): unknown => {
|
const parseJson = (text: string): unknown => {
|
||||||
if (!text.trim()) return {};
|
if (!text.trim()) return {};
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ import {
|
|||||||
clearOperationalTaskLease,
|
clearOperationalTaskLease,
|
||||||
operationalTaskClient,
|
operationalTaskClient,
|
||||||
releaseOperationalTaskCredential,
|
releaseOperationalTaskCredential,
|
||||||
scadaPresentationContinuation,
|
createScadaPresentationContinuation,
|
||||||
} from "../product/operational-task-client.js";
|
} from "../product/operational-task-client.js";
|
||||||
import { type SessionTranscriptStore } from "../sessions/transcriptStore.js";
|
import { type SessionTranscriptStore } from "../sessions/transcriptStore.js";
|
||||||
import { type SessionUiStateStore } from "../sessions/uiStateStore.js";
|
import { type SessionUiStateStore } from "../sessions/uiStateStore.js";
|
||||||
@@ -692,6 +692,7 @@ export const registerChatStreamRoute = (
|
|||||||
let taskHeartbeatPromise: Promise<void> | undefined;
|
let taskHeartbeatPromise: Promise<void> | undefined;
|
||||||
let taskLeaseFailure: Error | undefined;
|
let taskLeaseFailure: Error | undefined;
|
||||||
let taskRunFinalized = false;
|
let taskRunFinalized = false;
|
||||||
|
const continueScadaPresentation = createScadaPresentationContinuation();
|
||||||
const stopTaskHeartbeat = async () => {
|
const stopTaskHeartbeat = async () => {
|
||||||
taskHeartbeatActive = false;
|
taskHeartbeatActive = false;
|
||||||
if (taskHeartbeat) clearInterval(taskHeartbeat);
|
if (taskHeartbeat) clearInterval(taskHeartbeat);
|
||||||
@@ -819,7 +820,7 @@ export const registerChatStreamRoute = (
|
|||||||
if (task.lifecycle_status === "failed") throw new Error("Server rejected incomplete investigation artifacts");
|
if (task.lifecycle_status === "failed") throw new Error("Server rejected incomplete investigation artifacts");
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const continuation = scadaPresentationContinuation(error);
|
const continuation = continueScadaPresentation(error);
|
||||||
if (continuation) return continuation;
|
if (continuation) return continuation;
|
||||||
if (error && typeof error === "object" && "code" in error && error.code === "PENDING_AGENT_MESSAGES") {
|
if (error && typeof error === "object" && "code" in error && error.code === "PENDING_AGENT_MESSAGES") {
|
||||||
await messaging?.flush(binding.sessionId);
|
await messaging?.flush(binding.sessionId);
|
||||||
|
|||||||
@@ -27,6 +27,17 @@ it("continues an incomplete SCADA presentation with the exact missing device ins
|
|||||||
expect(scadaPresentationContinuation(new Error("transport failed"))).toBeNull();
|
expect(scadaPresentationContinuation(new Error("transport failed"))).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("bounds repeated incomplete-presentation continuations within one run", async () => {
|
||||||
|
const { createScadaPresentationContinuation } = await import("../../src/product/operational-task-client.js");
|
||||||
|
const resume = createScadaPresentationContinuation();
|
||||||
|
const rejection = new OperationalTaskClientError(409, "缺少设备:524529", "SCADA_PRESENTATION_INCOMPLETE");
|
||||||
|
expect(resume(new Error("transport failed"))).toBeNull();
|
||||||
|
for (let attempt = 0; attempt < 3; attempt++) expect(resume(rejection)).toContain("524529");
|
||||||
|
expect(resume(rejection)).toBeNull();
|
||||||
|
expect(resume(rejection)).toBeNull();
|
||||||
|
expect(createScadaPresentationContinuation()(rejection)).toContain("524529");
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
it("keeps all-null current chunks aligned to their own first slot across midnight", () => {
|
it("keeps all-null current chunks aligned to their own first slot across midnight", () => {
|
||||||
for (const [start, end, labels, times] of [
|
for (const [start, end, labels, times] of [
|
||||||
|
|||||||
Reference in New Issue
Block a user