Office Romeo: Patch-Based Document Editing and One Router Across Two Runtimes

Romi Nur Ismanto
Independent AI Research Lab, Jakarta, Indonesia
hello@rominur.com
September 2026

Abstract

Office Romeo, deployed at office.rominur.com, is a browser office suite — document, spreadsheet, presentation and PDF editors under one shell — whose assistant edits the file that is open rather than answering beside it. The distinction is the point of the system. An assistant that replies in a chat pane leaves the user to copy, paste and reconcile; an assistant that regenerates the whole document to change one paragraph destroys the parts nobody asked it to touch. Office Romeo takes a third path: the model is constrained to return a small JSON patch against the live canvas — a replacement for the current selection, a fragment to append, a cell write, a colour, a slide to add or update — and is instructed to prefer the smallest patch that accomplishes the request. Full-document replacement exists but is reserved for requests that genuinely mean the whole document. This paper describes four decisions supporting that design. First, a framework-neutral router: a single handleApi function returning {status, body, headers} is mounted twice, as Vite middleware in development and as serverless functions in production, so both environments execute identical routing code and the “works locally, 404 in production” class of bug is structurally excluded. Second, a latency budget computed against the platform's function ceiling: a three-model fallback chain runs under a total budget below the cap, with a per-model timeout, reasoning mode explicitly disabled because a canvas patch needs neither the latency nor the output tokens, and an early exit on authentication and billing failures where retrying cannot help. Third, a defensive reader for model output, since a model told to emit JSON will occasionally wrap it in prose or a fence. Fourth, a spreadsheet engine that is a real evaluator — tokeniser, parser, AST interpreter, sixty functions, memoised pull-based dependency walk, cycle detection and the standard error values — because a copilot that writes =SUM(D2:D6) into a grid that cannot evaluate it has not done the work. Documents are stored per user under AES-256-GCM envelope encryption, so the object store holds only ciphertext.

Keywords: AI-assisted editing, patch protocols, structured model output, serverless architecture, latency budgeting, spreadsheet evaluation, formula engines, envelope encryption, HMAC sessions, contentEditable, React, Vite

1. Introduction

The common shape of an AI writing assistant is a chat panel beside a document. It is easy to build and it moves the work to the wrong place: the model produces text in one region and the user performs the actual edit by hand in another, reconciling two versions that neither party owns completely. The obvious remedy — let the model rewrite the document — substitutes a worse failure. Asked to fix a heading, a model given the whole document returns a whole document, and the paragraphs it silently reworded on the way are discovered later or not at all.

Office Romeo is built on the position that the unit of an AI edit should be a patch, and that the patch should be as small as the request allows. The model does not hold the document; the canvas does. What the model returns is a description of a change, expressed in fields the client knows how to apply to a specific region of a specific editor. A request to recolour a selection produces a colour. A request to summarise produces a fragment to append. Only a request that means the entire document — change the tone of everything — produces a full replacement.

The suite spans four editors sharing one shell: a document editor over contentEditable, a spreadsheet with its own formula engine, a slide editor, and a PDF page editor, with mail and chat surfaces alongside. The whole application is roughly nine and a half thousand lines of JavaScript and JSX with four API routes behind it.

2. One Router, Two Runtimes

Development and production usually run different server code. In a Vite project the dev server is middleware and production is a set of platform functions, and the routing logic gets written twice — which means it gets to disagree with itself. The disagreement surfaces as the most tedious bug in the genre: an endpoint that works on the developer's machine and returns 404 after deploy.

Office Romeo writes the router once. handleApi({ method, path, body, headers }) knows nothing about any framework and returns a plain { status, body, headers }. Two thin adapters mount it. A Vite plugin attaches it as middleware under /api for both the dev server and the preview server, additionally hydrating .env files without overriding real environment variables and capping request bodies. A serverless adapter wraps the same function as a platform handler, normalising body parsing across the several shapes a request body can arrive in.

Vite middleware  →  handleApi  ←  serverless function

Each production route file is then a single line that binds a path to the shared router. One of them is worth noting for a small piece of care: the authentication route is a dynamic segment, and rather than parsing the URL again it reconstructs the logical path from the platform's own parsed query parameter, so the router receives the same canonical path shape in both runtimes.

3. The Patch Protocol

The system prompt fixes the response as a JSON object with a known set of optional fields and one required field, and instructs the model to include only the fields it actually sets. The instruction that carries the design is explicit: prefer the smallest patch; use a selection replacement for a selection, an append for added content, a colour for formatting; send full HTML only when the whole document must change.

Table 1. Patch fields and the edit each expresses.
FieldApplies toEffect
messageallRequired. Short explanation shown in the panel.
selectionHtmldoc, slides, pdfReplaces only the highlighted range. Preferred whenever a selection exists.
appendHtmldoc, pdfAppends a fragment — the route a summary takes into the file itself.
htmldoc, pdfFull replacement. Reserved for whole-document rewrites.
cellssheetZero-indexed row/column writes carrying a literal or a formula.
formatssheetBold, fill, colour, number format per cell.
colordoc, sheet, slides, pdfFont colour over the selection, or the used range when there is none.
addSlide / updateSlideslidesAdds a slide from a layout, or patches the current one.
notesslides, pdfSpeaker notes or page annotations.

Selection awareness is stated as a rule rather than left to inference: when the context carries a non-empty selection, the model is told to revise or analyse only that selection, to set the selection replacement and specifically not the full-document field. This is the instruction that prevents the characteristic failure of document assistants, in which highlighting one sentence and asking for a rewrite returns a rewritten essay.

The client applies patches through per-editor handlers, and every HTML-bearing field is sanitised before it reaches the canvas — the document, slide and PDF editors each pass model fragments through a sanitiser before insertion, since model output is untrusted input like any other.

4. Budgeting a Call Against the Function Ceiling

The Copilot route is the only one given a raised duration limit, and the client of that limit is written to respect it rather than to discover it. A three-model fallback chain runs under a total budget deliberately set below the platform ceiling, with a shorter per-model timeout, and the loop stops entering new attempts when too little of the budget remains to finish one.

Table 2. Budget parameters and the reasoning behind each.
ParameterSettingReason
Function duration30 sDeclared per-route; only the Copilot endpoint needs it.
Total chain budget26 sLeaves headroom so the whole fallback chain finishes inside the ceiling.
Per-model timeout20 sBounds one attempt; the remaining budget shortens it near the end.
Minimum to start2 sA new attempt is not begun when it cannot plausibly finish.
Reasoning modedisabledMultiplies latency and consumes the output budget; a canvas patch needs neither.
Response formatJSON objectStructured output requested at the API level, not only in the prompt.
Temperature0.3Patches are edits, not drafts.
Early exitauth / billing statusRetrying a rejected key or an exhausted balance cannot succeed.

The early exit is the detail most often missing from fallback chains. A chain that retries every model on a 401 turns one configuration error into three failed calls and three times the latency before reporting the same problem. Failures are also translated before they surface: a message matching exhausted balance or quota becomes a plain statement that the assistant is temporarily unavailable, rather than a provider error string shown to a user who can do nothing with it.

Request inputs are bounded on the way in as well — the prompt is truncated, the serialised file context is truncated, and only the last few turns of history are forwarded — so a large document cannot push the request past what the model or the function can absorb.

5. Reading What the Model Actually Returns

Structured output is requested at the API level and again in the prompt, and a model will still occasionally wrap its JSON in a markdown fence or precede it with a sentence. The reader is written for that reality rather than against it: it strips a fenced block when one is present, then takes the substring between the first opening brace and the last closing brace, and parses that. When parsing fails at any stage it does not throw — it returns the raw text as the message field, so the user sees what the model said instead of an error.

const fenced = raw.match(/```(?:json)?\s*([\s\S]*?)```/i)
const candidate = fenced ? fenced[1].trim() : raw
const start = candidate.indexOf('{')
const end = candidate.lastIndexOf('}')
if (start < 0 || end <= start) return { message: raw }

The degradation path matters more than the parsing. A malformed patch costs the user an edit; a thrown exception costs them the response entirely, including any explanation of why the edit was not possible.

6. Deterministic Intent Beside the Model

Some intents are cheap to recognise without a model and expensive to get wrong with one. Font colour is the example the codebase singles out. A small module matches colour words in Indonesian and English against a palette, gated by a context test so that the word “merah” inside prose does not trigger a recolour of the document.

Its second function is more interesting: it detects refusals. A model asked to change a font colour sometimes answers that it cannot edit files and suggests opening a desktop application — a response that is both false here and useless. When a colour intent is detected and the returned message matches a refusal pattern, the patch is repaired: the colour is filled in and the message replaced with a statement of what was done. The deterministic layer does not replace the model; it catches a specific, recognisable way the model fails and completes the request anyway.

7. A Spreadsheet Engine, Not a Formula Evaluator

A copilot that writes formulas into a grid that cannot evaluate them produces a screenshot, not a spreadsheet. The sheet editor is backed by a genuine evaluator: a tokeniser, a parser producing an AST, and an interpreter, in roughly seven hundred lines.

Table 3. Properties of the formula engine.
PropertyImplementation
FunctionsAround sixty, including SUM, AVERAGE, COUNTIF, SUMIF, VLOOKUP, INDEX, MATCH, RANK, STDEV, MEDIAN, IFERROR, TEXTJOIN, SUBSTITUTE and the date parts.
EvaluationLazy pull-based walk from each cell through its references, memoised in a map for the duration of one recalculation.
Cycle detectionA visiting set; re-entry on an in-progress cell yields the cycle error rather than recursing.
ErrorsThe standard set: cycle, division by zero, reference, value and name.
DatesExcel serial numbers on the 1899-12-30 epoch, rendered in UTC so a serial does not shift by a day across time zones.
DisplayLocale-aware formatting for currency, accounting, percent, scientific, date, time and plain number, with configurable decimals.
LiteralsLeading apostrophe forces text; trailing percent divides by a hundred; leading-zero strings are preserved rather than coerced to numbers.

The leading-zero rule is a small piece of practical care. A cell containing an identifier that begins with zero is text, not a number that has lost a digit — the defect that has quietly corrupted spreadsheets for as long as spreadsheets have existed.

8. Storage: Envelope Encryption at Rest

Files are stored per user, keyed by a hash of the account identifier rather than the identifier itself, and the payload is encrypted before it leaves the server. The scheme is AES-256-GCM with a random initialisation vector per write, the authentication tag stored alongside, and a key derived from the server secret through a hash with a purpose string so that the storage key and the session key are not the same value.

The consequence is that the object store holds ciphertext only. Writes are timestamp-named and stale objects are removed after the new one lands, which gives a simple write-then-replace ordering: a failure between the two leaves the previous version intact rather than a truncated current one. The same module falls back to an encrypted file on disk when no object-store token is configured, so local development exercises the identical encryption path rather than a plaintext shortcut.

9. Sessions Without a Token Library

Sessions are a signed cookie implemented directly: a base64url payload carrying subject, profile and expiry, joined to an HMAC-SHA256 signature over that payload. Verification compares signatures with a constant-time comparison, guarded by a length check first — the comparison primitive throws on unequal lengths, so the guard is required rather than defensive. The cookie is HTTP-only with same-site restriction and a thirty-day lifetime, and the secure attribute is set from the forwarded protocol and host so that local development over plain HTTP still works.

Sign-in verifies a Google identity token against Google's own endpoint and checks the audience against the configured client, the issuer against both accepted spellings, the presence of a subject, and the expiry — with a request timeout and a length cap on the submitted credential. The trade-off is deliberate and worth naming: validating against the endpoint is simpler than verifying a signature against a rotating key set locally, at the cost of a network round trip on every sign-in.

10. Sanitising Model Output

Every HTML-bearing patch field is sanitised before insertion. The sanitiser parses into an inert template element — content parsed there is not executed and does not fetch — removes script, frame, object, embed, link, meta and style elements, then walks the remaining attributes and strips event handlers and script-scheme URLs.

The honest description of this control is that it is a denylist, and denylists over HTML are a category with a long history of bypasses. The current implementation checks a small set of URL-bearing attributes and compares against a trimmed value, which is not equivalent to the normalisation a browser's own URL parser performs. Moving to an allowlist of elements and attributes, and normalising values before comparison, is the correct direction and is the principal outstanding item in this part of the codebase. The threat model is not hypothetical: a document assistant processes text the user did not write, so anything the model emits should be treated as attacker-influenced input.

11. Limits

The suite is a working model of an office application, not a replacement for one. Export covers plain text, comma-separated values and HTML; it does not write the binary formats whose extensions the interface displays, so a file leaves in a form other tools can read but not in the format its name implies.

Usage of the assistant is not yet metered or attributed per account, which is the next piece of work: an endpoint that spends money on behalf of whoever calls it needs both an identity check and a budget, in that order, and the design for both already exists elsewhere in the author's work. Storage is bounded by a per-user payload ceiling, which embedded images reach quickly. Two dependencies are declared as floating latest versions rather than pinned, so two builds of the same commit can differ. And the grid is fixed at a modest size, evaluated in full on each recalculation — adequate at that size, and not an approach that survives a much larger sheet.

12. Conclusion

The argument of Office Romeo is that an assistant inside a document should return an edit, not a draft, and that the smaller the edit the more useful it is. Constraining the model to a patch vocabulary makes the assistant composable with an editor the user still controls: the selection stays the selection, the untouched paragraphs stay untouched, and the failure of one patch costs one edit rather than the document.

Two supporting decisions generalise beyond this application. Writing the router once and mounting it in both runtimes removes an entire class of environment-drift bug for the cost of one indirection. And budgeting a model call against the platform's own ceiling — total budget, per-attempt timeout, a floor below which a new attempt is not begun, and an early exit where retrying cannot help — converts an unbounded external dependency into something that fails predictably and on time.

References

  1. Fielding, R., and Reschke, J. “Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content.” RFC 7231, 2014.
  2. Barth, A. “HTTP State Management Mechanism.” RFC 6265, 2011.
  3. West, M., and Goodwin, M. “Same-Site Cookies.” IETF Draft, 2016.
  4. Krawczyk, H., Bellare, M., and Canetti, R. “HMAC: Keyed-Hashing for Message Authentication.” RFC 2104, 1997.
  5. Josefsson, S. “The Base16, Base32, and Base64 Data Encodings.” RFC 4648, 2006.
  6. Dworkin, M. “Recommendation for Block Cipher Modes of Operation: Galois/Counter Mode (GCM) and GMAC.” NIST SP 800-38D, 2007.
  7. Barker, E., and Roginsky, A. “Recommendation for Key Derivation Methods in Key Establishment Schemes.” NIST SP 800-56C, 2020.
  8. Sakimura, N., et al. “OpenID Connect Core 1.0.” OpenID Foundation, 2014.
  9. Jones, M., Bradley, J., and Sakimura, N. “JSON Web Token (JWT).” RFC 7519, 2015.
  10. Bray, T. “The JavaScript Object Notation (JSON) Data Interchange Format.” RFC 8259, 2017.
  11. WHATWG. “HTML Living Standard — The template element and contenteditable.” 2026.
  12. WHATWG. “URL Living Standard.” 2026.
  13. OWASP Foundation. “Cross Site Scripting Prevention Cheat Sheet.” 2026.
  14. OWASP Foundation. “OWASP Top 10 for Large Language Model Applications.” 2026 revision.
  15. Greshake, K., et al. “Not What You've Signed Up For: Compromising Real-World LLM-Integrated Applications with Indirect Prompt Injection.” AISec, 2023.
  16. Aho, A. V., Lam, M. S., Sethi, R., and Ullman, J. D. Compilers: Principles, Techniques, and Tools. 2nd ed., Pearson, 2006.
  17. Pratt, V. R. “Top Down Operator Precedence.” POPL, 1973.
  18. Ecma International. “Office Open XML File Formats.” ECMA-376, 5th edition, 2016.
  19. Ziv, G., and Panko, R. “What We Know About Spreadsheet Errors.” Journal of End User Computing, 1998.
  20. Ismanto, R. N. “Pikapiku: Client-Side Chunking and a No-Retention Schema for Long-Form Transcription on Serverless Infrastructure.” 2026.
  21. Ismanto, R. N. “Jaipong: A Two-Stage Text-to-Song Studio and the Case for a Line-Oriented Plan Format.” 2026.