diff --git a/CHANGELOG.md b/CHANGELOG.md index 06c21e5..82277c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ All notable user-facing changes to **omp-spt** (the Spacetime adapter for oh-my- > Each release below is keyed to the **adapter version of truth** (the version `spt adapter list` reports and the GitHub release tag). omp-spt starts its own version line at 0.1.0; the sections from [0.17.3] down are the inherited **claude-spt** lineage this project forked from, retained for history. +## [0.3.8] - 2026-07-19 + +> Requires spt-core **v0.31.0 or newer** and Oh My Pi **v16.3.15 or newer**. Update with `spt adapter update omp-spt`, then restart existing endpoints so they load the corrected listener and session identity. + +### Fixed +- **Incoming messages containing quoted SPT events no longer shut the endpoint down.** The listener now preserves complete `…` text inside a peer message, while incomplete frames remain bounded and fail closed. Deferred messages also use OMP's follow-up queue so a streaming-state race cannot silently lose them. +- **The inline OMP status now shows the complete endpoint identity.** The footer reads ` @ (/)` in cyan instead of `spt:`, matching the session and terminal-window naming introduced in v0.3.7. + ## [0.3.7] - 2026-07-19 > Requires spt-core **v0.31.0 or newer** and Oh My Pi **v16.3.15 or newer**. Update with `spt adapter update omp-spt`, then restart existing endpoints so they load the new title behavior. diff --git a/README.md b/README.md index f6aa87c..0dc4777 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ release with its bundled native extension and OMP plugin skills. ## Install -The v0.3.7 release asset supports **x86_64 Windows and x86_64 Linux only**. It +The v0.3.8 release asset supports **x86_64 Windows and x86_64 Linux only**. It contains Windows MSVC, Linux GNU, and static Linux musl x86_64 helpers, but no macOS or Arm64 payload. The musl helper is a compatibility tier for OMP-capable hosts; it is not a generic Alpine support claim. Pick a supported shell below; diff --git a/adapter/strings/omp-spt.mjs b/adapter/strings/omp-spt.mjs index ba7abfa..463621f 100644 --- a/adapter/strings/omp-spt.mjs +++ b/adapter/strings/omp-spt.mjs @@ -4,6 +4,8 @@ const ADAPTER = "omp-spt"; const BUSY_TITLE_GLYPHS = [..."⣾⣽⣻⢿⡿⣟⣯⣷"]; const IDLE_TITLE_GLYPH = "○"; const TITLE_FRAME_MS = 80; +const CYAN = "\u001b[36m"; +const DEFAULT_FOREGROUND = "\u001b[39m"; // [impl->REQ-OMP-SESSION-TITLES] export function endpointDisplayName(id, node, project) { @@ -16,6 +18,10 @@ export function endpointDisplayName(id, node, project) { : `${endpoint} @ ${nodeName}`; } +export function endpointInlineStatus(id, node, project) { + return `${CYAN}${endpointDisplayName(id, node, project)}${DEFAULT_FOREGROUND}`; +} + export function decodeBody(body) { return body .replaceAll("
", "\n") @@ -58,15 +64,24 @@ function parseEventTag(tag) { return { attributes }; } -function findValidNestedEvent(raw, bodyStart, close) { - let candidate = raw.indexOf("= 0 && (close < 0 || candidate < close)) { - const candidateEnd = raw.indexOf(">", candidate); - if (candidateEnd < 0 || (close >= 0 && candidateEnd >= close)) return -1; - if (!parseEventTag(raw.slice(candidate, candidateEnd)).error) return candidate; - candidate = raw.indexOf("", cursor); + if (close < 0) return -1; + if (open >= 0 && open < close) { + const openEnd = raw.indexOf(">", open); + if (openEnd < 0 || openEnd >= close) return -1; + if (!parseEventTag(raw.slice(open, openEnd)).error) depth += 1; + cursor = openEnd + 1; + continue; + } + depth -= 1; + if (depth === 0) return close; + cursor = close + "".length; } - return -1; } export function drainEvents(raw, options = {}) { @@ -95,15 +110,7 @@ export function drainEvents(raw, options = {}) { rest: raw.slice(start), }; } - const close = raw.indexOf("", openEnd + 1); - const nested = findValidNestedEvent(raw, openEnd + 1, close); - if (nested >= 0) { - return { - error: protocolError("nested EVENT before closing the current frame"), - events, - rest: raw.slice(start), - }; - } + const close = findEventClose(raw, openEnd + 1); if (close < 0) { if (raw.length - start > maxFrameChars) { return { @@ -1172,7 +1179,7 @@ export function createOmpSpt(overrides = {}) { } activated = true; startupBriefPending = true; - ui.setStatus("omp-spt", `spt:${id}`); + ui.setStatus("omp-spt", endpointInlineStatus(id, env.OMP_SPT_NODE, env.OMP_SPT_PROJECT)); // [impl->REQ-OMP-SESSION-TITLES] pi.setSessionName(displayName()); showIdleTitle(); @@ -1639,7 +1646,7 @@ export function createOmpSpt(overrides = {}) { item.stub = senderStub(item.from ?? "unknown"); item.submitted = true; try { - pi.sendUserMessage(item.stub); + pi.sendUserMessage(item.stub, { deliverAs: "followUp" }); } catch (error) { item.submitted = false; await rejectItem(item, "could not submit your message to OMP", error); @@ -1767,7 +1774,7 @@ export function createOmpSpt(overrides = {}) { const status = signal ? `signal ${signal}` : code; died(new Error(`spt api listen exited ${status}`), true); }); - ui?.setStatus("omp-spt", `spt:${id}`); + ui?.setStatus("omp-spt", endpointInlineStatus(id, env.OMP_SPT_NODE, env.OMP_SPT_PROJECT)); } // [impl->REQ-OMP-NATIVE-TUI] diff --git a/docs-site/llms-full.txt b/docs-site/llms-full.txt index 602aef4..8e32928 100644 --- a/docs-site/llms-full.txt +++ b/docs-site/llms-full.txt @@ -78,7 +78,7 @@ You need [Oh My Pi 16.3.15 or newer](https://github.com/can1357/oh-my-pi/commit/ and [`spt-core`](https://sabermage.github.io/spt-releases). There is no separate plugin installation step because the adapter release carries its OMP plugin skills. -The v0.3.7 `omp-spt` release supports **x86_64 Windows and x86_64 Linux only**. +The v0.3.8 `omp-spt` release supports **x86_64 Windows and x86_64 Linux only**. Its archive contains Windows MSVC, Linux GNU, and static Linux musl x86_64 helpers, but no macOS or Arm64 payload. The musl helper is a compatibility tier for OMP-capable hosts; it is not a generic Alpine support claim. diff --git a/docs-site/src/quickstart.md b/docs-site/src/quickstart.md index 3a87c1f..4ad43fc 100644 --- a/docs-site/src/quickstart.md +++ b/docs-site/src/quickstart.md @@ -9,7 +9,7 @@ You need [Oh My Pi 16.3.15 or newer](https://github.com/can1357/oh-my-pi/commit/ and [`spt-core`](https://sabermage.github.io/spt-releases). There is no separate plugin installation step because the adapter release carries its OMP plugin skills. -The v0.3.7 `omp-spt` release supports **x86_64 Windows and x86_64 Linux only**. +The v0.3.8 `omp-spt` release supports **x86_64 Windows and x86_64 Linux only**. Its archive contains Windows MSVC, Linux GNU, and static Linux musl x86_64 helpers, but no macOS or Arm64 payload. The musl helper is a compatibility tier for OMP-capable hosts; it is not a generic Alpine support claim. diff --git a/docs/PARITY.md b/docs/PARITY.md index e14f4e9..8b5c04f 100644 --- a/docs/PARITY.md +++ b/docs/PARITY.md @@ -9,14 +9,14 @@ ## Versioned comparison baseline -Current baseline: `omp-spt v0.3.7` → `BigscreenVR/claude-spt-bs v0.25.1`. +Current baseline: `omp-spt v0.3.8` → `BigscreenVR/claude-spt-bs v0.25.1`. `omp-spt` v0.3.4 was published at 2026-07-16 10:48:22 UTC. The first repository commit after that release, `413d6908d3a514edf93e0e53d4fb1f4b46ff7269`, landed at 2026-07-19 09:49:10 UTC and fixes the start of the work interval that produced v0.3.5. `claude-spt-bs` v0.25.1 was the latest published sister release at that instant, published from `BigscreenVR/claude-spt-bs` at 2026-07-19 04:55:53 UTC. -This baseline identifies the exact sister behavior used to define v0.3.7 parity. It is not a +This baseline identifies the exact sister behavior used to define v0.3.8 parity. It is not a claim that later `claude-spt` capabilities are covered. Any subsequent feature-parity work must first select and record the exact published `claude-spt` release being consulted. Every parity-informed `omp-spt` release must update this baseline to its own version; the release diff --git a/tests/omp-extension.mjs b/tests/omp-extension.mjs index 5af4c92..fd9a9e0 100644 --- a/tests/omp-extension.mjs +++ b/tests/omp-extension.mjs @@ -3,6 +3,7 @@ import { EventEmitter } from "node:events"; import { createOmpSpt, endpointDisplayName, + endpointInlineStatus, decodeBody, drainEvents, parsePeerShortforms, @@ -108,6 +109,7 @@ function createHarness(options = {}) { const calls = []; const children = []; const submitted = []; + const submittedDeliveries = []; const statuses = []; const notifications = []; const errors = []; @@ -206,9 +208,10 @@ function createHarness(options = {}) { sentMessages.push({ message, delivery }); options.onSendMessage?.(message, delivery); }, - sendUserMessage(content) { + sendUserMessage(content, delivery) { submitted.push(content); - options.onSubmit?.(content); + submittedDeliveries.push(delivery); + options.onSubmit?.(content, delivery); }, }; const runSptCommand = async (args, input, runOptions) => { @@ -303,6 +306,7 @@ function createHarness(options = {}) { sessionNames, titles, submitted, + submittedDeliveries, tools, get shutdowns() { return shutdowns; @@ -335,6 +339,10 @@ async function testParsing() { ); assert.equal(endpointDisplayName("emphasys", "HFENDULEAM"), "emphasys @ HFENDULEAM"); assert.equal(endpointDisplayName("emphasys", undefined, "omp-spt"), "emphasys"); + assert.equal( + endpointInlineStatus("emphasys", "HFENDULEAM", "omp-spt"), + "\u001b[36memphasys @ HFENDULEAM (omp-spt/)\u001b[39m", + ); const partialEnvelope = 'hello
wo'; const partial = drainEvents(`noise${partialEnvelope}`); @@ -358,12 +366,15 @@ async function testParsing() { }, ]); - const truncatedA = - 'truncatedvalid'; - const nested = drainEvents(truncatedA); - assert.deepEqual(nested.events, []); - assert.equal(nested.rest, truncatedA); - assert.match(nested.error.message, /nested EVENT/); + const nestedEventBody = + 'quoted valid tail'; + assert.deepEqual(drainEvents(nestedEventBody).events, [ + { + from: "a", + body: 'quoted valid tail', + envelope: nestedEventBody, + }, + ]); assert.match( drainEvents('missing sender').error.message, /missing EVENT from/, @@ -391,6 +402,10 @@ async function testEndpointSessionNameAndAnimatedWindowTitle() { await harness.emit("session_start"); assert.deepEqual(harness.sessionNames, ["emphasys @ HFENDULEAM (omp-spt/)"]); assert.deepEqual(harness.titles, ["○ emphasys @ HFENDULEAM (omp-spt/)"]); + assert.equal( + harness.statuses.at(-1).text, + "\u001b[36memphasys @ HFENDULEAM (omp-spt/)\u001b[39m", + ); await harness.emit("agent_start"); assert.equal(harness.titles.at(-1), "⣾ emphasys @ HFENDULEAM (omp-spt/)"); @@ -1060,13 +1075,13 @@ async function testProtocolCorruptionFailsClosed() { return harness; } - const truncatedA = - 'truncatedvalid'; - const nested = await failProtocol(truncatedA, /nested EVENT/); - assert.deepEqual( - commandCalls(nested, "send").map((call) => call.args[1]), - [], - "the later valid b frame must not be merged into or consumed as a", + await failProtocol( + 'truncatedvalid'.padEnd( + 128, + "x", + ), + /buffer exceeded/, + { listenerBufferLimit: 96 }, ); await failProtocol('missing sender', /missing EVENT from/); await failProtocol('bad attrs', /malformed EVENT attributes/); @@ -1631,6 +1646,11 @@ async function testActiveTurnBoundaryDeliveryAndFallback() { [''], "a later accepted message must preserve order and fall back to an ordinary next turn", ); + assert.deepEqual( + harness.submittedDeliveries, + [{ deliverAs: "followUp" }], + "listener delivery must survive OMP streaming-state races", + ); assertNoAgentManagedPoll(harness); await harness.emit("session_shutdown"); }