94f2536a | fix(backend): keep chat alive when default-workflow installation fails | Amal | 2026-08-12 | ↗ GitHub |
commit body WHY THIS MATTERS
buildWorkflowStore() awaited ensureDefaultWorkflows(), which throws on any
RPC error, on every chat message - and the chat routes call it OUTSIDE
their try/catch blocks. Reproduced live: dropping the RPC (equivalent to
deploying code before the migration, or one database hiccup) and sending a
single chat message printed UnhandledPromiseRejection and the Node process
exited. The entire backend went down for every user, and the next message
after a restart killed it again.
WHAT IS AN UNHANDLED REJECTION CRASH?
Express 4 route handlers are plain functions; if you pass an async function
and it throws before you reach your own try block, nothing catches the
rejected promise. Since Node 15, an unhandled promise rejection terminates
the process by default. That turns "one bad query" into "the whole API is
down". (Express 5 forwards async errors to the error middleware; this repo
is on Express 4, so every bare async handler carries this risk.)
HOW THE FIX WORKS
Installing defaults is a convenience, not a prerequisite for chatting, so
the call is now best-effort: failures are logged and the chat proceeds with
whatever workflows already exist. The user-visible defaults still install
on the next successful list/quick-action request, whose asyncRoute wrapper
already converts errors into a 500 JSON response instead of a crash.
Found by fault-injection review of PR #309.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E4PXCdenNH5Mqhm5Sre9Zs
|
bdb2b4b6 | perf(backend): install defaults and sync the add-on catalog once per process | Amal | 2026-08-12 | ↗ GitHub |
commit body WHY THIS MATTERS
Two hot-path costs shipped with the workflows restructure, measured live:
1. ensureDefaultWorkflows() ran on EVERY workflow list, quick-action list,
and chat message. After first install the RPC is a pure no-op, yet each
call shipped the full five-default payload (multi-KB of prompt text) to
Postgres and took pg_advisory_xact_lock(user), serializing all of one
user's concurrent requests through that transaction forever.
2. syncWorkflowAddonCatalog() ran on every GET /workflow-addons and upserted
all ~129 catalog rows (142 KB response, full prompt bodies) with a fresh
updated_at on each request. One browser refresh of the Workflows page
rewrote every row - verified by watching min/max(updated_at) advance in
the database on each reload. The content_hash column existed precisely to
prevent this but was computed, stored, and never compared.
WHAT IS A PER-PROCESS LATCH?
The catalog is derived from a generated module that only changes when new
code deploys, and a deploy restarts the process. So "sync once per process
lifetime" is exactly as fresh as "sync on every request" - module-level
state (a cached promise) makes the first request do the work and every
later caller await the same result. Concurrent first requests share one
sync instead of racing duplicate upserts; a failed sync clears the latch so
the next request retries rather than caching the failure.
HOW THE FIX WORKS
- ensureDefaultWorkflows() remembers per process which users it has already
ensured and skips the RPC afterwards. Failures are not cached.
- syncWorkflowAddonCatalog() runs behind the latch, reads the stored
(addon_key, content_hash, active) rows first, and only upserts seeds whose
hash differs - so updated_at now changes only when content actually
changes, and the steady-state sync is a single SELECT.
- New unit tests pin all of this: RPC called once per user, failures retried,
hash match skips the upsert, concurrent callers share one sync, and the
latch clears after an error.
Found by review of PR #309 (catalog rewrite measured via psql before/after
page reloads).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E4PXCdenNH5Mqhm5Sre9Zs
|
6c3d0fd5 | fix(frontend): retry quick-action migration on failure; reopen Templates tab | Amal | 2026-08-12 | ↗ GitHub |
commit body WHY THIS MATTERS
Two regressions in the assistant home's quick actions:
1. The one-shot localStorage-to-database migration stamped its "done" marker
even when the updateQuickAction() calls failed. One transient API error
during a user's first load after upgrading would permanently discard the
quick-action preferences they had set in the old localStorage system -
silent, unrecoverable data loss of user configuration.
2. "Draft from Template" used to open the document picker on the Templates
tab (initialDocumentTab: "templates" in the pre-database quick action).
The database-backed rewrite dropped the option, so the picker opened on
Files - replicated live: click the quick action, the Add Documents modal
opens with Files pressed and the user's templates a tab away.
WHAT IS A ONE-SHOT MIGRATION MARKER?
A flag ("mike.quickActions.databaseMigrated") that says "the legacy state
has been carried over, never look at it again". Such a marker must only be
written after the migration actually succeeded; writing it on failure turns
a retryable error into permanent loss. The updates are idempotent, so
retrying a partial batch on the next load is safe.
HOW THE FIX WORKS
- The marker is now only written when no migration update was rejected; on
partial failure the merged state still renders but the marker stays unset
so the next load retries.
- handleQuickAction() passes initialDocumentTab: "templates" for the
template-drafting default again. The workflow title is the only stable
handle the quick-action row exposes today (the same handle the migration
itself matches on); if the user renames their copy the picker simply
falls back to Files.
Found by review of PR #309 (Templates-tab regression captured on video).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E4PXCdenNH5Mqhm5Sre9Zs
|
ed735ea1 | fix(frontend): make workflow-list failures visible and independent | Amal | 2026-08-12 | ↗ GitHub |
commit body WHY THIS MATTERS
Four failure paths in the rewritten WorkflowList were invisible or worse:
1. The initial load used Promise.all over the user's workflows AND the new
add-on catalog, so a failing /workflow-addons endpoint blanked the whole
workflows page - data that loaded fine before this feature existed.
2. importAddon() had no error handling at all: a failed import flipped the
button back from "Importing..." with zero feedback, leaving the user to
wonder whether they now own a copy (an unhandled promise rejection in
the console was the only trace).
3. Bulk delete/import wrote "Some selected ... could not be ..." into
loadError, which only renders inside the table's EMPTY state - invisible
while rows exist, then leaking into the empty state much later.
4. openAddon() set the preview modal from a fetch with no staleness check:
close the modal while the request is in flight and it pops back open;
open A then B quickly and the modal can swap back to A's content.
WHAT IS THE PATTERN HERE?
Every fetch needs an answer to "what does the user see when this fails or
resolves late?" Promise.allSettled loads independent datasets independently;
a ref that records which add-on is currently open lets late responses be
recognized as stale and dropped; action errors get a dedicated, dismissible
banner that renders regardless of table contents.
HOW THE FIX WORKS
- Load: allSettled over (workflows, add-ons); each dataset renders or
reports its own error (loadError for workflows, addonsError shown in the
Add-ons tab).
- importAddon: in-flight guard (second click is a no-op), try/catch, and a
visible "Could not import ..." banner on failure.
- Bulk actions report into the new actionError banner above the table.
- openAddon/closeAddon track the open add-on id in a ref and ignore stale
responses.
Found by review of PR #309.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E4PXCdenNH5Mqhm5Sre9Zs
|
5ed658b4 | fix(word-addin): open reference downloads in the system browser | Amal | 2026-08-12 | ↗ GitHub |
commit body WHY THIS MATTERS
The new Assets download button called window.open() directly. In Office
task-pane webviews - notably desktop Word - window.open is blocked, so the
click did nothing, with no error shown. This add-in already documents the
problem: ApiKeyBanner.tsx says "window.open is blocked in some hosts" and
uses the sanctioned escape hatch. New code has to follow the same rule or
the feature silently works only in a browser tab.
WHAT IS Office.context.ui.openBrowserWindow?
Office.js's supported way to open a URL from inside the task-pane sandbox:
it asks the host application (Word) to launch the user's default browser.
window.open stays as the fallback for environments without Office.js (the
hermetic e2e bundle, plain-browser development).
HOW THE FIX WORKS
- Download now resolves the signed URL and hands it to a small
openExternalUrl() helper mirroring the ApiKeyBanner pattern.
- Bonus hardening in the same component: the reference-file list effect now
carries a cancelled flag (fast workflow switching can no longer render a
previous workflow's files, matching every other fetch effect in this PR)
and a failed list fetch surfaces its error instead of quietly rendering
"No reference files."
Found by review of PR #309.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E4PXCdenNH5Mqhm5Sre9Zs
|
b2116375 | fix(backend): harden the workflow and quick-action routes | Amal | 2026-08-12 | ↗ GitHub |
commit body WHY THIS MATTERS
The new routers were correct on the happy path but leaked or mis-handled
several edge cases a public API will hit:
- Quick actions created against a shared workflow survived share
revocation: the list path re-fetched workflow titles by bare id with no
access re-check, so a revoked user kept a live row (title included) for a
workflow they can no longer open. withWorkflowDetails now verifies the
caller still owns or has a share for each workflow and drops
inaccessible quick actions (PATCH returns 404 in that case).
- sort_order above 2^31-1 passed Number.isInteger and blew up as a 500 on
the int4 column; it is now a 400 with a clear message. WHAT IS int4?
Postgres integer is 32-bit - API validation must enforce the column's
real range, not JavaScript's.
- Workflow DELETE removed R2 storage objects BEFORE the DB delete; a DB
failure left rows pointing at deleted storage. Order flipped: DB rows
first, storage cleanup only for rows actually removed (same
partial-failure logic as the replication commit).
- Neither new router had the tail error middleware the workflows router
has, so thrown errors rendered Express's HTML 500 instead of the API's
{detail} JSON contract. Both routers now share the pattern.
- POST /workflow-addons/:addonId/import performs storage downloads and
uploads but was missing from the uploadLimiter list; added.
- Add-on import inserted explicit nulls for language/practice/
jurisdictions, bypassing the column defaults every other creation path
gets; now coalesced to 'English' / 'General Transactions' / ['General'].
- Removed a provably-dead filter (SYSTEM_WORKFLOW_IDS can never match DB
UUIDs) that implied system rows could appear in the database.
Found by review of PR #309.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E4PXCdenNH5Mqhm5Sre9Zs
|
dcb85a1a | refactor(db): fold the no-op migration 03 into 01 while the branch is unmerged | Amal | 2026-08-12 | ↗ GitHub |
commit body WHY THIS MATTERS
Migration 20260811_03 dropped a NOT NULL that 01 never shipped and re-added
an identical foreign key - a complete no-op, because 01 had been rewritten
after 03 was authored. Landing both would leave the open-source migration
trail asking "which one is authoritative?" forever. Since none of these
migrations have shipped anywhere (they are all new in this PR), the honest
history is one final migration.
WHAT IS THE RULE FOR EDITING MIGRATIONS?
Migrations are append-only once released, freely editable before. This
branch is unmerged, so 01 is still editable; after merge, semantics changes
would require a new dated migration.
HOW
- 20260811_03_deletable_default_workflows.sql deleted; 01 already contains
its end state (nullable workflow_id, on delete set null).
- The redundant default_workflow_installations_user_idx dropped from 01 and
schema.sql - the unique(user_id, default_key) constraint's index already
serves user_id lookups. Postgres implements unique constraints as
b-tree indexes; a separate single-column index on the same leading
column is pure write overhead.
Found by review of PR #309.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E4PXCdenNH5Mqhm5Sre9Zs
|
feb2e55f | fix(backend): make document replication atomic, honest, and safely worded | Amal | 2026-08-12 | ↗ GitHub |
commit body WHY THIS MATTERS
replicate_document inserted library rows with status "ready" BEFORE
uploading any bytes. A failed upload left user-visible broken documents
(rows with no version) in Library Files; a failed current_version_id update
was silently swallowed (Supabase builders return errors in-band - they do
not reject) while the tool still reported ok:true. The repo's own
persistGeneratedFile does it right: bytes first, rows last, so the worst
failure is an invisible orphaned blob, never a broken row the user can see.
WHAT IS UPLOAD-FIRST ORDERING?
When an operation spans storage and database, order the writes so each
partial-failure state is harmless: pre-generate the document UUIDs, upload
all bytes under those ids, then insert documents + versions, then link
current_version_id checking every in-band error. Copies whose linking fails
are rolled back (best-effort row delete) and reported in a new
failed_copies array; if every copy fails the result is ok:false.
ALSO IN THIS COMMIT (same pipeline, same files)
- Raw Postgres/S3 error text no longer reaches the model/user event stream:
the three fail() sites now wrap errors with safeErrorMessage, matching
the streaming path's convention (secrets/endpoints stay out of chat).
- Within-turn workflow reference handles use the full workflow id instead
of id.slice(0,8) - every "builtin-*" id shared that prefix, so two system
workflows' assets could silently rebind each other's doc handles.
- Dead DocStore.source_id field removed (written once, read nowhere).
- The prompt mandated replicate → edit_document for ALL templates, but
edit_document only supports .docx; a PDF template boxed the model in with
no permitted fallback. The instruction is now scoped to .docx copies,
with explicit guidance for other types, and the per-file notices match
the prompt's "will be edited or filled in" scope so purely-informational
reference files no longer trigger spurious library copies.
- Tests now pin the contract: uploads are asserted to happen before any
documents insert (call-order log), with the exact storage key and bytes,
plus a failure path proving no "ready" row survives a failed upload.
Found by review of PR #309.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E4PXCdenNH5Mqhm5Sre9Zs
|
0ca6c460 | ci: fail the build when the generated workflow catalog drifts from its source | Amal | 2026-08-12 | ↗ GitHub |
commit body WHY THIS MATTERS
With 13k generated lines, no reviewer can eyeball whether a regeneration of
systemWorkflows.ts is faithful. This was flagged in PR #256 and deferred;
PR #309 tripling the file's size makes it overdue.
HOW IT WORKS
The new workflows-drift job checks out this repo and mike-workflows as
siblings (the layout the generator expects), pins mike-workflows to the
commit stamped inside the generated file (grepped from
SYSTEM_WORKFLOWS_SOURCE_COMMIT), reruns the zero-dependency generator, and
fails on `git diff --exit-code` over the generated outputs. Any hand-edit,
stale regeneration, or locale-dependent ordering difference turns the PR
red instead of shipping silently.
Found by review of PR #309.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E4PXCdenNH5Mqhm5Sre9Zs
|
3a4480f2 | docs: describe the real defaults/add-ons/packs model in CONTRIBUTING | Amal | 2026-08-12 | ↗ GitHub |
commit body WHY THIS MATTERS
CONTRIBUTING.md still told workflow contributors to set
metadata.mike-availability to "system" to publish a system workflow. That
contract died in the workflows restructure: a contributor following the
docs would see their workflow silently ship as an add-on instead, with no
error anywhere. Stale contributor docs are how an open-source project
burns its first-time contributors.
WHAT THE DOCS NOW SAY
Five defaults are hardcoded in DEFAULT_WORKFLOW_IDS
(backend/src/lib/workflowCatalog.ts) and install once per user as
editable, deletable copies; every other repository workflow ships in the
Add-ons catalog and imports as an independent copy; packs come from
pack.yaml, which must list exactly the workflow directories it contains;
mike-availability is deprecated and ignored; CI regenerates the catalog
from the stamped source commit and fails on drift.
Found by review of PR #309.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E4PXCdenNH5Mqhm5Sre9Zs
|
946e4978 | build(workflows): stamp source provenance and retire the dead availability flag | Amal | 2026-08-12 | ↗ GitHub |
commit body WHY THIS MATTERS
backend/src/lib/systemWorkflows.ts is 2.2 MB of generated code whose source
of truth lives in a different repository (mike-workflows), with nothing
recording WHICH commit it was generated from and no way to detect a stale
or hand-edited regeneration. Separately, the mike-availability frontmatter
flag became dead data in this PR: 31 workflows are marked "system" but the
default/add-on split is really the hardcoded DEFAULT_WORKFLOW_IDS list -
two contradictory sources of truth for a contract contributors rely on.
WHAT IS PROVENANCE FOR GENERATED CODE?
A committed generated file is only reviewable if a machine can reproduce
it: record the exact source commit in the output, and let CI regenerate
from that commit and diff. The generator now runs `git rev-parse HEAD` in
the mike-workflows checkout, stamps it into the file header, and exports
SYSTEM_WORKFLOWS_SOURCE_COMMIT for tooling to read.
DECISION ENCODED HERE
The app owns which workflows install as defaults (a product decision, in
code, reviewed in this repo); repository metadata does not. Therefore the
generator stops emitting `availability` (the key is still accepted with a
deprecation warning so existing content builds) and the generated file
drops all 135 availability lines. A new provenance test pins the contract:
the stamped commit matches /^[0-9a-f]{40}$/ and every DEFAULT_WORKFLOW_IDS
entry exists in the generated catalog.
ALSO
- pack.yaml validation is now bidirectional: a workflow directory present
under a pack but missing from pack.workflows fails the build (previously
a WIP folder would silently ship as part of the pack).
- localeCompare calls pin the "en" locale so regeneration is byte-stable
across machines with different ICU collations.
- Regeneration verified: before these changes the committed file reproduced
byte-for-byte from mike-workflows @ 4b9c7cd; after, the diff is exactly
the header/constant plus the removed availability lines.
Found by review of PR #309.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E4PXCdenNH5Mqhm5Sre9Zs
|
2c1b6214 | fix(frontend): sort indicators reflect the effective sort, not just the explicit one | Amal | 2026-08-12 | ↗ GitHub |
commit body WHY THIS MATTERS
DocTable's new defaultSort participated in row ordering
(sort ?? defaultSort) but the header indicators only read `sort`. In the
Library (defaultSort updated/desc) every column claimed "Default Order"
while rows were actually date-sorted, and choosing "Default Order" could
never restore insertion order because null falls straight back to the
default - the menu lied twice.
HOW IT WORKS
A single hoisted effectiveSort feeds ordering, header indicators, and the
folder-name comparator, so they cannot disagree again. Where a defaultSort
exists the reset option is labeled "Default (Updated)" - truthful about
what it restores - and surfaces the effective direction as checked.
Consumers without defaultSort are byte-identical (effectiveSort === sort).
Found by review of PR #309.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E4PXCdenNH5Mqhm5Sre9Zs
|
5dfd2650 | fix(word-addin): give add-ons real load/error states; stop silent quick-action saves | Amal | 2026-08-12 | ↗ GitHub |
commit body WHY THIS MATTERS
Three silent-failure paths in the add-in's new surfaces:
1. Add-on import errors were written into the SHARED fetchError state,
which WorkflowList renders on the other tab - the user saw a workflow
list error after failing to import an add-on. Imports now have their
own error state rendered where the action happened.
2. A failed listWorkflowAddons collapsed to an empty array: a blank tab
with no message. The tab now has loading, error, and "No add-ons
available." states - and requests ?type=assistant server-side (the
backend supports it) instead of over-fetching and filtering.
3. Quick-action "Done" failures were fully silent: the modal just refused
to close. Saves now show a Saving state and a role="alert" error.
Toggling Active also no longer writes the modal's UNSAVED draft into
list state - the list takes the server's row; only the modal keeps the
draft.
WHAT IS THE PARITY RULE?
The add-in mirrors the web app's UX (project convention): same
error-surfacing expectations, same server-side filtering, same "the server
response is the truth for list state" reconciliation.
Found by review of PR #309.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E4PXCdenNH5Mqhm5Sre9Zs
|
baba1840 | fix(frontend): confirm asset deletion and serialize reference uploads | Amal | 2026-08-12 | ↗ GitHub |
commit body WHY THIS MATTERS
Reference-file Delete acted immediately from a row menu - one misclick
permanently destroyed a file, while every other destructive action in this
feature (workflow delete, column delete) goes through ConfirmPopup.
Uploads had a matching gap: the toolbar button disabled during an upload
but the drag-drop path forwarded drops unconditionally, interleaving
batches and clobbering the shared warning text.
HOW IT WORKS
Delete now stages the file behind the same ConfirmPopup pattern as
workflow deletion. Uploads take a synchronous in-flight guard (a ref, so a
second drop in the same render frame is still caught), the detail page
stops accepting drops while a batch runs, and warnings append instead of
overwrite so an unsupported-file notice survives a later failure.
Found by review of PR #309.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E4PXCdenNH5Mqhm5Sre9Zs
|
cd6caeed | fix(frontend): reconcile quick-action updates per action, server wins | Amal | 2026-08-12 | ↗ GitHub |
commit body WHY THIS MATTERS
Two optimistic-update bugs could show the user state the server does not
have:
1. InitialView's edit merge spread the stale local row OVER the server
response ({ ...updated, ...item, ...changes }), so any server-side
normalization of prompt/document_upload never reached the UI until a
full reload.
2. The account Features page rolled back ALL toggles when ANY of its N
PATCHes failed - if 3 of 4 succeeded, the UI showed the opposite of
what the server now stores.
WHAT IS THE RECONCILIATION RULE?
An optimistic update may paint local state first, but when the server
answers, the response is the truth for the fields it covers; on partial
batch failure, keep the fulfilled updates and roll back only the rejected
ones, and say so ("Some quick actions could not be updated. Try again.").
Found by review of PR #309.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E4PXCdenNH5Mqhm5Sre9Zs
|
fc0f76eb | chore(frontend): delete the orphaned SelectAssistantProjectModal | Amal | 2026-08-12 | ↗ GitHub |
commit body WHY THIS MATTERS
The workflows restructure removed the three non-workflow quick actions
(start chat in project / new project / new tabular review) that were this
modal's only consumers - repo-wide grep confirms zero imports remain. Dead
components in an open-source tree actively mislead: the next contributor
assumes something renders it and reads it as living code.
THE PRODUCT DECISION (stated, not hidden)
Dropping those three quick actions is treated as intentional: they were
navigation shortcuts duplicating the sidebar, and the DB-backed quick
actions model is deliberately workflow-only. If that call is reversed,
this component should be rebuilt against the new model rather than
resurrected - its data flow predates the restructure.
Found by review of PR #309.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E4PXCdenNH5Mqhm5Sre9Zs
|
e9568c12 | test(workflows): reset default install cache between route tests | willchen96 | 2026-08-13 | ↗ GitHub |