fix: fence pending queue controls
This commit is contained in:
@@ -105,7 +105,7 @@ timestamp: number, provenance: SessionEntryProvenance, derived_from?: Array<stri
|
||||
|
||||
export type PendingSubmissionSummary = { submission_id: string, accepted_at_ms: number, segment_count: number, byte_len: number, };
|
||||
|
||||
export type PendingSubmissionsSnapshot = { revision: number, notification_count: number, submissions: Array<PendingSubmissionSummary>, };
|
||||
export type PendingSubmissionsSnapshot = { revision: number, notification_count: number, head_id: string | null, submissions: Array<PendingSubmissionSummary>, };
|
||||
|
||||
export type SubmissionDisposition = "started" | "queued";
|
||||
|
||||
@@ -231,7 +231,7 @@ export type SubscriptionFramePayload = { "frame": "request", "message": Subscrip
|
||||
|
||||
export type SubscriptionFrame = { protocol_version: number, } & ({ "frame": "request", "message": SubscriptionRequest } | { "frame": "response", "message": SubscriptionResponse } | { "frame": "event", "message": SubscriptionEvent } | { "frame": "worker_protocol", "message": SubscriptionWorkerProtocolMethod });
|
||||
|
||||
export type Method = { "method": "submit", "params": { submission_request_id: string, input: Array<Segment>, } } | { "method": "notify", "params": { notification_request_id: string, message: string, auto_run?: boolean, } } | { "method": "worker_event", "params": WorkerEvent } | { "method": "list_pending_submissions" } | { "method": "cancel_pending_submission", "params": { submission_id: string, } } | { "method": "clear_pending_submissions" } | { "method": "continue_pending" } | { "method": "resume" } | { "method": "cancel" } | { "method": "pause" } | { "method": "compact" } | { "method": "list_rewind_targets" } | { "method": "rewind_to", "params": { target: RewindTargetId, expected_head_entries: number, } } | { "method": "shutdown" } | { "method": "list_completions", "params": { kind: CompletionKind, prefix: string, } } | { "method": "list_workers" } | { "method": "restore_worker", "params": { name: string, } } | { "method": "register_peer", "params": { name: string, } };
|
||||
export type Method = { "method": "submit", "params": { submission_request_id: string, input: Array<Segment>, } } | { "method": "notify", "params": { notification_request_id: string, message: string, auto_run?: boolean, } } | { "method": "worker_event", "params": WorkerEvent } | { "method": "list_pending_submissions" } | { "method": "cancel_pending_submission", "params": { submission_id: string, expected_revision: number, } } | { "method": "clear_pending_submissions", "params": { expected_revision: number, } } | { "method": "continue_pending", "params": { expected_revision: number, expected_head_id: string, } } | { "method": "resume" } | { "method": "cancel" } | { "method": "pause" } | { "method": "compact" } | { "method": "list_rewind_targets" } | { "method": "rewind_to", "params": { target: RewindTargetId, expected_head_entries: number, } } | { "method": "shutdown" } | { "method": "list_completions", "params": { kind: CompletionKind, prefix: string, } } | { "method": "list_workers" } | { "method": "restore_worker", "params": { name: string, } } | { "method": "register_peer", "params": { name: string, } };
|
||||
|
||||
export type Event = { "event": "submission_accepted", "data": { submission_request_id: string, submission_id: string, disposition: SubmissionDisposition, } } | { "event": "submission_rejected", "data": { submission_request_id: string, message: string, } } | { "event": "pending_submissions_changed", "data": { pending: PendingSubmissionsSnapshot, } } | { "event": "user_message", "data": { segments: Array<Segment>, } } | { "event": "system_item", "data": { item: unknown, } } | { "event": "invoke_start", "data": { kind: InvokeKind, } } | { "event": "turn_start", "data": { turn: number, } } | { "event": "turn_end", "data": { turn: number, result: TurnResult, } } | { "event": "llm_call_start", "data": { llm_call: number, } } | { "event": "llm_call_end", "data": { llm_call: number, } } | { "event": "llm_retry", "data": { llm_call: number,
|
||||
/**
|
||||
|
||||
@@ -2150,7 +2150,12 @@ Deno.test("snapshot restores TaskStore state from system history", () => {
|
||||
const event = snapshotEvent("/repo");
|
||||
if (event.event !== "snapshot") throw new Error("snapshot fixture expected");
|
||||
event.data.session = {
|
||||
pending_submissions: { revision: 0, notification_count: 0, submissions: [] },
|
||||
pending_submissions: {
|
||||
revision: 0,
|
||||
notification_count: 0,
|
||||
head_id: null,
|
||||
submissions: [],
|
||||
},
|
||||
entries: [{
|
||||
entry_id: "task-reminder-1",
|
||||
timestamp: 1,
|
||||
|
||||
@@ -1077,7 +1077,20 @@ Deno.test("Web Console uses Notify while running and exposes durable pending con
|
||||
'method: "cancel_pending_submission"',
|
||||
'method: "clear_pending_submissions"',
|
||||
'method: "continue_pending"',
|
||||
"handleQueueSubmit",
|
||||
"handleNotifySubmit",
|
||||
">Queue Submit</button>",
|
||||
">Notify</button>",
|
||||
]) {
|
||||
assert(consolePage.includes(token), `missing durable pending control token: ${token}`);
|
||||
}
|
||||
|
||||
const userCase = consolePage.slice(
|
||||
consolePage.indexOf('case "user":'),
|
||||
consolePage.indexOf('case "compact":'),
|
||||
);
|
||||
assert(
|
||||
!userCase.includes("workerRunning"),
|
||||
"ordinary text must remain Submit instead of being implicitly converted to Notify",
|
||||
);
|
||||
});
|
||||
|
||||
+56
-16
@@ -161,6 +161,7 @@
|
||||
let pendingSubmissions = $state<PendingSubmissionsSnapshot>({
|
||||
revision: 0,
|
||||
notification_count: 0,
|
||||
head_id: null,
|
||||
submissions: [],
|
||||
});
|
||||
let pendingSubmissionItems = $derived(pendingSubmissions.submissions ?? []);
|
||||
@@ -574,16 +575,6 @@
|
||||
): ProtocolMethod {
|
||||
switch (request.kind) {
|
||||
case "user":
|
||||
if (workerRunning) {
|
||||
return {
|
||||
method: "notify",
|
||||
params: {
|
||||
notification_request_id: crypto.randomUUID(),
|
||||
message: request.content,
|
||||
auto_run: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
method: "submit",
|
||||
params: {
|
||||
@@ -672,6 +663,14 @@
|
||||
void submitDraft(composerInputElement?.snapshot() ?? draft);
|
||||
}
|
||||
|
||||
function handleQueueSubmit() {
|
||||
void submitDraft(composerInputElement?.snapshot() ?? draft);
|
||||
}
|
||||
|
||||
function handleNotifySubmit() {
|
||||
void submitDraft(composerInputElement?.snapshot() ?? draft, "notify");
|
||||
}
|
||||
|
||||
function attachmentPath(): string {
|
||||
return `/api/w/${encodeURIComponent(workspaceId)}/runtimes/${encodeURIComponent(runtimeId)}/workers/${encodeURIComponent(workerId)}`;
|
||||
}
|
||||
@@ -771,7 +770,15 @@
|
||||
if (event.dataTransfer?.files) addAttachmentFiles(event.dataTransfer.files);
|
||||
}
|
||||
|
||||
async function submitDraft(value: ComposerDraftSnapshot) {
|
||||
async function submitDraft(
|
||||
value: ComposerDraftSnapshot,
|
||||
delivery: "submit" | "notify" = "submit",
|
||||
) {
|
||||
if (delivery === "notify" && attachments.length > 0) {
|
||||
composerNotice = null;
|
||||
sendError = "Notify accepts text only; remove attachments or queue a Submit.";
|
||||
return;
|
||||
}
|
||||
const incompleteAttachment = attachments.find((attachment) =>
|
||||
attachment.state !== "uploaded" || !attachment.reference
|
||||
);
|
||||
@@ -805,10 +812,19 @@
|
||||
return;
|
||||
}
|
||||
|
||||
let request: WorkerConsoleInputRequest = command.request;
|
||||
if (delivery === "notify") {
|
||||
if (request.kind !== "user") {
|
||||
composerNotice = null;
|
||||
sendError = "Notify accepts ordinary text, not a Composer command.";
|
||||
return;
|
||||
}
|
||||
request = { kind: "notify", content: request.content };
|
||||
}
|
||||
sending = true;
|
||||
sendError = null;
|
||||
try {
|
||||
const method = composerRequestToProtocolMethod(command.request);
|
||||
const method = composerRequestToProtocolMethod(request);
|
||||
sendProtocolMethod(method);
|
||||
composerInputElement?.recordHistory(value);
|
||||
composerInputElement?.clear();
|
||||
@@ -1772,7 +1788,10 @@
|
||||
sendControl(
|
||||
{
|
||||
method: "cancel_pending_submission",
|
||||
params: { submission_id: submission.submission_id },
|
||||
params: {
|
||||
submission_id: submission.submission_id,
|
||||
expected_revision: pendingSubmissions.revision,
|
||||
},
|
||||
},
|
||||
"Pending submission cancellation",
|
||||
)}
|
||||
@@ -1782,10 +1801,16 @@
|
||||
</ol>
|
||||
<button
|
||||
type="button"
|
||||
disabled={workerRunning}
|
||||
disabled={workerRunning || pendingSubmissions.head_id === null}
|
||||
onclick={() =>
|
||||
sendControl(
|
||||
{ method: "continue_pending" },
|
||||
{
|
||||
method: "continue_pending",
|
||||
params: {
|
||||
expected_revision: pendingSubmissions.revision,
|
||||
expected_head_id: pendingSubmissions.head_id ?? "",
|
||||
},
|
||||
},
|
||||
"Pending activation continue",
|
||||
)}
|
||||
>Continue next</button>
|
||||
@@ -1793,7 +1818,10 @@
|
||||
type="button"
|
||||
onclick={() =>
|
||||
sendControl(
|
||||
{ method: "clear_pending_submissions" },
|
||||
{
|
||||
method: "clear_pending_submissions",
|
||||
params: { expected_revision: pendingSubmissions.revision },
|
||||
},
|
||||
"Pending submissions clear",
|
||||
)}
|
||||
>Clear all</button>
|
||||
@@ -1932,6 +1960,18 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="composer-actions">
|
||||
{#if workerRunning}
|
||||
<button
|
||||
type="button"
|
||||
disabled={sending || !inputReady}
|
||||
onclick={handleQueueSubmit}
|
||||
>Queue Submit</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={sending || !inputReady}
|
||||
onclick={handleNotifySubmit}
|
||||
>Notify</button>
|
||||
{/if}
|
||||
{#if composerNotice}
|
||||
<span class="composer-notice">{composerNotice}</span>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user