diff --git a/adapter/strings/omp-spt.mjs b/adapter/strings/omp-spt.mjs index c051d53..9d2a03c 100644 --- a/adapter/strings/omp-spt.mjs +++ b/adapter/strings/omp-spt.mjs @@ -98,154 +98,120 @@ export function drainEvents(raw, options = {}) { rest: raw.slice(start), }; } return { events, rest: raw.slice(start) }; } const end = close + "".length; if (end - start > maxFrameChars) { return { error: protocolError(`EVENT frame exceeded ${maxFrameChars} characters`), events, rest: raw.slice(start), }; } const parsed = parseEventTag(raw.slice(start, openEnd)); if (parsed.error) return { error: parsed.error, events, rest: raw.slice(start) }; if (parsed.attributes.type === "msg") { events.push({ from: parsed.attributes.from, body: decodeBody(raw.slice(openEnd + 1, close)), envelope: raw.slice(start, end), }); } cursor = end; if (events.length >= maxEvents) return { events, rest: raw.slice(cursor) }; } } function messageText(message) { if (typeof message?.content === "string") return message.content; return (message?.content ?? []) .filter((part) => part?.type === "text" && typeof part.text === "string") .map((part) => part.text) .join(""); } -export function extractReply(messages, afterUserMessage) { - const allMessages = messages ?? []; - let start = 0; - if (afterUserMessage !== undefined) { - const userIndex = allMessages.findLastIndex((message) => { - if (message?.role !== "user") return false; - const text = messageText(message); - return text === afterUserMessage || text.startsWith(`${afterUserMessage}\n\n message?.role === "assistant"); - return assistant ? messageText(assistant) : ""; -} function assistantMessageIdentity(message) { if (message?.role !== "assistant") return undefined; if (typeof message.responseId === "string" && message.responseId) { return `response:${message.provider ?? ""}:${message.model ?? ""}:${message.responseId}`; } if (Number.isFinite(message.timestamp)) { return `timestamp:${message.provider ?? ""}:${message.model ?? ""}:${message.timestamp}`; } return undefined; } function captureAssistantBaseline(messages) { const assistants = (messages ?? []).filter((message) => message?.role === "assistant"); const identities = new Set(); for (const message of assistants) { const identity = assistantMessageIdentity(message); if (identity !== undefined) identities.add(identity); } return { count: assistants.length, identities }; } function assistantAfterBaseline(messages, baseline) { const assistants = (messages ?? []).filter((message) => message?.role === "assistant"); for (let index = assistants.length - 1; index >= 0; index -= 1) { const identity = assistantMessageIdentity(assistants[index]); if (identity !== undefined && !baseline.identities.has(identity)) return assistants[index]; } for (let index = assistants.length - 1; index >= baseline.count; index -= 1) { if (assistantMessageIdentity(assistants[index]) === undefined) return assistants[index]; } return undefined; } const SUCCESSFUL_ASSISTANT_STOP_REASONS = new Set(["stop", "length", "toolUse"]); function successfulAssistant(message) { return SUCCESSFUL_ASSISTANT_STOP_REASONS.has(message?.stopReason); } -function unsuccessfulAssistantPayload(message) { - if (!message) return failureMessage("turn ended without an assistant response"); - if (message.stopReason === "aborted") { - return failureMessage("assistant turn was aborted before completion", message.errorMessage); - } - if (message.stopReason === "error") { - return failureMessage("assistant turn failed before completion", message.errorMessage); - } - return failureMessage( - `assistant turn ended without a successful completion (${message.stopReason ?? "missing stopReason"})`, - ); -} function firstLine(text) { return text.split(/\r?\n/).find((line) => line.trim()) ?? ""; } function errorSummary(error) { const detail = error instanceof Error ? error.message : String(error); return firstLine(detail).trim() || "unknown error"; } -function failureMessage(reason, error) { - const detail = error === undefined ? "" : `: ${errorSummary(error)}`; - return `[omp-spt] ${reason}${detail}`; -} function senderStub(sender) { const escaped = sender .replaceAll("&", "&") .replaceAll('"', """) .replaceAll("<", "<") .replaceAll(">", ">"); return ``; } function injectEnvelope(messages, item) { const index = messages.findLastIndex( (message) => message?.role === "user" && messageText(message) === item.stub, ); if (index < 0) return messages; const original = messages[index]; const content = typeof original.content === "string" ? `${original.content}\n\n${item.envelope}` : [...(original.content ?? []), { type: "text", text: `\n\n${item.envelope}` }]; const injected = [...messages]; injected[index] = { ...original, content }; return injected; } function maskMarkdownCode(text) { const source = String(text); const masked = source.split(""); let fenceCharacter; let fenceLength = 0; let lineStart = 0; while (lineStart < source.length) { const newline = source.indexOf("\n", lineStart); const lineEnd = newline < 0 ? source.length : newline + 1; const line = source.slice(lineStart, newline < 0 ? lineEnd : newline).replace(/\r$/, ""); @@ -631,73 +597,70 @@ export function createOmpSpt(overrides = {}) { overrides.shortformCommandTimeoutMs ?? DEFAULT_SHORTFORM_COMMAND_TIMEOUT_MS; const shortformConcurrency = DEFAULT_SHORTFORM_CONCURRENCY; const runSptCommand = customRunSptCommand ?? ((args, input, options = {}) => runSpt(args, input, { clearTimeout: clearTimer, commandTimeoutMs: options.timeoutMs ?? commandTimeoutMs, env, killForceMs, killGraceMs, setTimeout: setTimer, signal: options.signal, spawnProcess, })); const shutdownBudgetMs = overrides.shutdownBudgetMs ?? DEFAULT_SHUTDOWN_BUDGET_MS; const requestedShutdownCommandTimeoutMs = overrides.shutdownCommandTimeoutMs ?? DEFAULT_SHUTDOWN_COMMAND_TIMEOUT_MS; const terminationWindowMs = killGraceMs + killForceMs; const maxShutdownCommandTimeoutMs = Math.max( 1, Math.floor((shutdownBudgetMs - 3 * terminationWindowMs) / 2), ); const shutdownCommandTimeoutMs = Math.max( 1, Math.min(requestedShutdownCommandTimeoutMs, maxShutdownCommandTimeoutMs), ); const listenerBufferLimit = overrides.listenerBufferLimit ?? DEFAULT_LISTENER_BUFFER_LIMIT; const acceptedQueueLimit = overrides.acceptedQueueLimit ?? DEFAULT_ACCEPTED_QUEUE_LIMIT; const acceptedBytesLimit = overrides.acceptedBytesLimit ?? DEFAULT_ACCEPTED_BYTES_LIMIT; const restartDelaysMs = [...(overrides.restartDelaysMs ?? [250, 1000, 4000])]; - const outcomeRetryDelaysMs = [ - ...(overrides.outcomeRetryDelaysMs ?? [250, 1000, 4000]), - ]; const sessionEndRetryDelaysMs = [ ...(overrides.sessionEndRetryDelaysMs ?? [250, 1000]), ]; const listenerStableMs = overrides.listenerStableMs === false ? undefined : (overrides.listenerStableMs ?? 30_000); const checkUpdates = overrides.checkUpdates ?? true; const updateProbeTimeoutMs = overrides.updateProbeTimeoutMs ?? 1_500; const fetchLatestAdapterVersion = overrides.fetchLatestAdapterVersion ?? (async () => { const controller = new AbortController(); const timer = setTimer(() => controller.abort(), updateProbeTimeoutMs); timer?.unref?.(); try { const response = await fetch( "https://api.github.com/repos/BigscreenVR/omp-spt/releases/latest", { headers: { accept: "application/vnd.github+json" }, signal: controller.signal, }, ); if (!response.ok) return undefined; return (await response.json())?.tag_name; } catch { return undefined; } finally { clearTimer(timer); } }); const checkpointParameters = (pi) => { const z = pi.zod?.z ?? pi.zod; return z.object({ wake: z .string() @@ -1406,203 +1369,138 @@ export function createOmpSpt(overrides = {}) { function endSession() { if (!sid || !token) return Promise.resolve(); if (!endPromise) { const operation = (async () => { await stateOperation.catch(() => {}); await runCommand(["api", "--adapter", ADAPTER, "session-end", id, ...authArgs()]); endpointState = undefined; })(); endPromise = operation; void operation.catch(() => { if (endPromise === operation) endPromise = undefined; }); } return endPromise; } async function endSessionWithRetry() { for (let attempt = 0; ; attempt += 1) { try { await endSession(); return; } catch (error) { if (shutdownMode || attempt >= sessionEndRetryDelaysMs.length) throw error; const delay = sessionEndRetryDelaysMs[attempt]; pi.logger.error( `omp-spt session teardown failed; retrying ${ attempt + 1 }/${sessionEndRetryDelaysMs.length} in ${delay}ms`, { error: errorSummary(error) }, ); await waitForRetry(delay); } } } - // [impl->REQ-OMP-EXTENSION-CUSTODY] - function settleItem(item, payload) { - if (!item) return Promise.resolve(); - if (item.outcomePromise) return item.outcomePromise; - item.settling = true; - item.outcomePromise = (async () => { - if (!item.from) throw new Error("missing EVENT from attribute"); - for (let attempt = 0; ; attempt += 1) { - try { - await runCommand(["send", item.from, "--from", id], payload); - item.settled = true; - return; - } catch (error) { - if (shutdownMode || attempt >= outcomeRetryDelaysMs.length) throw error; - const delay = outcomeRetryDelaysMs[attempt]; - pi.logger.error( - `omp-spt could not send the outcome to ${item.from}; retrying ${ - attempt + 1 - }/${outcomeRetryDelaysMs.length} in ${delay}ms`, - { error: errorSummary(error) }, - ); - await waitForRetry(delay); - } - } - })(); - return item.outcomePromise; - } function releaseItem(item) { if (!item?.accounted) return; item.accounted = false; acceptedBytes -= item.acceptedBytes; } function beginListenerTermination(child, label) { if (listenerTerminationPromise) return listenerTerminationPromise; const operation = (async () => { try { await terminateChild(child, label, { clearTimer, forceMs: killForceMs, graceMs: killGraceMs, setTimer, }); } catch (error) { pi.logger.error("omp-spt could not reap the listener", { error: errorSummary(error), }); } })(); listenerTerminationPromise = operation; void operation.then(() => { if (listenerTerminationPromise === operation) listenerTerminationPromise = undefined; }); return operation; } async function stopResources() { if (dispatchTimer !== undefined) { clearTimer(dispatchTimer); dispatchTimer = undefined; } if (restartTimer !== undefined) { clearTimer(restartTimer); restartTimer = undefined; } if (listenerStableTimer !== undefined) { clearTimer(listenerStableTimer); listenerStableTimer = undefined; } const child = listener; listener = undefined; listenerBuffer = ""; if (child) { await beginListenerTermination(child, "spt api listener"); } else { await listenerTerminationPromise; } } - async function settlePendingItem(item, reason) { - try { - if (item.outcomePromise && !item.settled) { - let existingError; - try { - await item.outcomePromise; - } catch (error) { - existingError = error; - } - if (item.settled) return; - if (existingError && !shutdownMode) { - logError( - `omp-spt could not return custody to ${item.from ?? "unknown"}`, - existingError, - ); - return; - } - item.outcomePromise = undefined; - item.settling = false; - } - try { - await settleItem(item, failureMessage(reason)); - } catch (error) { - logError(`omp-spt could not return custody to ${item.from ?? "unknown"}`, error); - } - } finally { - releaseItem(item); - } - } - - async function failPending(reason) { + function releasePending() { const pending = current ? [current, ...queue] : [...queue]; if (overflowItem) pending.push(overflowItem); current = undefined; queue.length = 0; overflowItem = undefined; - for (let index = 0; index < pending.length; index += 1) { - if (shutdownMode) { - await Promise.all( - pending.slice(index).map((item) => settlePendingItem(item, reason)), - ); - return; - } - await settlePendingItem(pending[index], reason); - } + for (const item of pending) releaseItem(item); } function teardownSession(pendingReason) { if (!teardownPromise) { stopping = true; const operation = (async () => { await stopResources(); - await failPending(pendingReason); + releasePending(); await bindPromise?.catch(() => {}); await endSessionWithRetry(); })(); teardownPromise = operation; void operation.catch(() => { if (teardownPromise === operation) teardownPromise = undefined; }); } return teardownPromise; } async function shutdownWithinBudget(pendingReason) { enterShutdownMode(); const teardown = teardownSession(pendingReason); let budgetTimer; const expired = new Promise((resolve) => { budgetTimer = setTimer(() => { budgetTimer = undefined; shutdownDeadlineExpired = true; const error = new Error( `omp-spt shutdown exceeded its ${shutdownBudgetMs}ms budget`, ); abortActiveCommands(error); for (const finish of [...retryWaiters]) finish(); resolve(false); }, shutdownBudgetMs); budgetTimer?.unref?.(); }); const completed = teardown.then( () => true, (error) => { logError("omp-spt session teardown failed", error); return true; }, ); @@ -1623,77 +1521,70 @@ export function createOmpSpt(overrides = {}) { ui?.setStatus("omp-spt", "spt failed"); logError(message, error); try { await teardownSession("endpoint stopped before your message could complete"); } catch (teardownError) { logError("omp-spt session teardown failed", teardownError); } runtimeCtx?.shutdown(); })(); return fatalPromise; } function scheduleDispatch() { if ( stopping || agentActive || dispatching || current || queue.length === 0 || dispatchTimer !== undefined ) { return; } dispatchTimer = setTimer(() => { dispatchTimer = undefined; void dispatchNext().catch((error) => { if (!stopping) return failClosed("omp-spt dispatch failed", error); }); }, 0); dispatchTimer?.unref?.(); } async function rejectItem(item, reason, error) { if (stopping) return; logError(`omp-spt ${reason}`, error); - try { - await settleItem(item, failureMessage(reason, error)); - } catch (outcomeError) { - if (stopping) return; - await failClosed(`omp-spt could not send the outcome to ${item.from ?? "unknown"}`, outcomeError); - return; - } if (current === item) { current = undefined; releaseItem(item); } if (!stopping) { desiredState = "idle"; try { await setState("idle"); } catch (stateError) { await failClosed( "omp-spt could not restore idle state after a failed submission", stateError, ); } } } // [impl->REQ-OMP-EXTENSION-CUSTODY] async function dispatchNext() { if (stopping || agentActive || dispatching || current || queue.length === 0) return; dispatching = true; const item = queue.shift(); current = item; try { try { desiredState = "busy"; await setState("busy"); } catch (error) { await rejectItem(item, "could not accept your message", error); return; } if (stopping) return; if (agentActive) { if (current === item) current = undefined; queue.unshift(item); @@ -1860,71 +1751,71 @@ export function createOmpSpt(overrides = {}) { }; pi.on("session_before_switch", (event, ctx) => blockSessionChange(`${event.reason} session switch`, ctx), ); pi.on("session_before_branch", (_event, ctx) => blockSessionChange("session branch", ctx), ); // [impl->REQ-OMP-MESSAGE-CONTEXT] // [impl->REQ-PARITY-SAFE-BOUNDARY-DELIVERY] pi.on("context", (event) => { const baseline = captureAssistantBaseline(event.messages); if (agentActive) { if (!turnContextObserved) { turnAssistantBaseline = baseline; turnContextObserved = true; } } else { observedAssistantBaseline = baseline; } let messages = event.messages; if ( agentActive && !current && !dispatching && queue.length > 0 ) { const item = queue.shift(); item.stub = senderStub(item.from ?? "unknown"); item.submitted = true; item.activeInjected = true; item.assistantBaseline = baseline; current = item; } - if (current?.submitted && !current.settling) { + if (current?.submitted) { if (current.activeInjected) { const content = `${current.stub}\n\n${current.envelope}`; if ( !messages.some( (message) => message?.role === "user" && messageText(message) === content, ) ) { messages = [...messages, { role: "user", content }]; } } else { current.assistantBaseline ??= baseline; messages = injectEnvelope(messages, current); } } if (messages !== event.messages) return { messages }; }); // [impl->REQ-PARITY-STARTUP-BRIEF] // [impl->REQ-PARITY-TARGETED-HINTS] // [impl->REQ-PARITY-UPDATE-NOTICE] pi.on("before_agent_start", (event) => { if (!activated || !id || stopping) return; const additions = []; if (startupBriefPending) { startupBriefPending = false; additions.push(startupBrief(id)); } const hints = promptHints(event.prompt); if (hints.length > 0) additions.push(`OMP SPT targeted hints:\n- ${hints.join("\n- ")}`); if (updateNoticesPending && updateNoticesReady) { updateNoticesPending = false; if (updateNotices.length > 0) { additions.push(`OMP SPT updates:\n- ${updateNotices.join("\n- ")}`); } } firstTurnContextStarted = true; @@ -1947,93 +1838,75 @@ export function createOmpSpt(overrides = {}) { async ({ target, body }) => { if (!id) return `${target}: failed (activate this OMP session first)`; try { const result = await runCommand(["send", target, "--from", id], body, { timeoutMs: shortformCommandTimeoutMs, }); return `${target}: ${firstLine(result) || "sent"}`; } catch (error) { return `${target}: failed (${errorSummary(error)})`; } }, ); if (stopping) return; try { pi.sendMessage( { customType: "omp-spt-peer-status", content: `OMP SPT peer-message results:\n- ${statuses.join("\n- ")}`, display: true, attribution: "user", }, { deliverAs: "nextTurn", triggerTurn: true }, ); } catch (error) { if (!stopping) logError("omp-spt could not inject peer-message results", error); } } // [impl->REQ-HAZARD-ABNORMAL-TURN-RECEIVABILITY] async function completeTurn(event) { agentActive = false; desiredState = "idle"; if (stopping) return; const messages = event.messages ?? []; - const completed = current; - const completedBaseline = completed?.assistantBaseline ?? turnAssistantBaseline; const currentTurnAssistant = assistantAfterBaseline(messages, turnAssistantBaseline); - const completedAssistant = assistantAfterBaseline(messages, completedBaseline); + const completed = current; const currentTurnReply = successfulAssistant(currentTurnAssistant) ? messageText(currentTurnAssistant) : ""; - if (completed?.submitted && !completed.settled) { - const reply = messageText(completedAssistant); - const payload = successfulAssistant(completedAssistant) - ? reply || failureMessage("turn completed without a textual assistant response") - : unsuccessfulAssistantPayload(completedAssistant); - try { - await settleItem(completed, payload); - } catch (error) { - if (stopping) return; - await failClosed( - `omp-spt could not send the outcome to ${completed.from ?? "unknown"}`, - error, - ); - return; - } - } if (current === completed) { current = undefined; releaseItem(completed); } observedAssistantBaseline = captureAssistantBaseline(messages); try { await setState("idle"); } catch (error) { if (stopping) return; await failClosed("omp-spt could not mark the endpoint idle", error); return; } await dispatchShortforms(currentTurnReply); if (!stopping) scheduleDispatch(); } pi.on("agent_start", async () => { if (stopping) return; turnCompletionPromise = undefined; turnAssistantBaseline = observedAssistantBaseline; turnContextObserved = false; agentActive = true; desiredState = "busy"; try { await syncDesiredState(); } catch (error) { if (!stopping) await failClosed("omp-spt could not mark the endpoint busy", error); } }); // [impl->REQ-OMP-EXTENSION-CUSTODY] pi.on("agent_end", (event) => { turnCompletionPromise ??= completeTurn(event); return turnCompletionPromise; });