Bot responses to messages inside Lark topic threads were dropped
outside the thread because outbound Send always used the new-message
endpoint. This change:
- Adds LarkClient.ReplyMessage() that POSTs to
/open-apis/im/v1/messages/{id}/reply with reply_in_thread=true
- Parses thread_id from im.message.receive_v1 events (distinct from
root_id which fires on any quote reply) and stamps
feishu_reply_target_id into the message metadata
- Propagates the key through cmd/gateway_consumer_normal.go and the
new package-level routingMetaKeys var in internal/channels/events.go
so block replies and retry notifications also land in thread
- Routes sendText, sendMarkdownCard, sendImage, sendFile, and
sendMediaAttachment via a new deliverMessage helper that falls back
to SendMessage with a warning log on reply endpoint errors (e.g.
thread root deleted)
- Adds 10 unit tests covering routing, fallback, content
double-encoding, and the thread_id gate that prevents plain quote
replies from being silently promoted to threads
Closes#818
Go regexp's $1_ is parsed as a reference to a named group "1_" (underscores are
valid identifier chars), not $1 followed by literal underscore. This dropped the
captured italic content, turning <i>foo</i> into "__" instead of "_foo_".
Use ${1}_ with explicit braces to delimit the group reference, and update the
format_extended_test suite to assert the full round-trip for <i> and <em>.
Document test coverage improvement initiative:
- 43 new test files across 3 waves
- Per-package coverage floors in coverage_thresholds.json
- CI ratchet gate prevents regression
- ~9000 lines of new test code, 61 packages covered
- Update vault.ts: add VaultDocument union type, metadata field in VaultLink
- Update vault-graph-adapter.ts: map document nodes to green color node class
- Update vault-detail-dialog.tsx: handle isBinary branch for download vs preview
- Update vault-document-sidebar.tsx: filter by type with document type icon + label
- Update vault-documents-table.tsx: render document type badge + path_basename column
- Add i18n translations: type.document + type.document_desc across en/vi/zh
Pin that a pre-hardening fragmented entry written under the raw UUID
cache key (tenantID:<uuidStr>) is still evicted by the TTL branch in
Router.Get when a UUID-form caller arrives after TTL expiry. The test
synthesizes the fragmented entry directly via a test-only map write,
then asserts the subsequent Get evicts the raw-UUID entry, re-invokes
the resolver once, and writes the canonical tenantID:agentKey entry —
leaving no fragmented entries behind.
handleUpdate accepted any string in the agent_key allowlist field
without running it through isValidSlug. A client could rename an
agent to "weird:key", which would break router cache exact-segment
invalidation (the cache splits on the last colon for invalidation
matching). Add the slug check inline after the allowlist filter and
return MsgInvalidSlug on failure. The slug regex already rejects
colons, slashes, whitespace, and other characters that would confuse
path rendering or cache key parsing — add a dedicated predicate test
covering the full trap surface.
The existing tests exercise only the DB fallback path via errorAgentStore.
Add a test that primes the router cache with a stub implementing both
agent.Agent and agentUUIDProvider, then asserts the cache-aware helper
returns the cached UUID without touching the store — the raison d'etre
of the helper. Uses the same sentinel-error store so a broken fast path
surfaces as a sentinel error rather than a silent pass.
insertDedupCandidate and ScanDuplicates both swallowed uuid.Parse
errors before feeding entity IDs to the INSERT. Currently safe
because all inputs originate from DB SELECT rows or caller-validated
UUIDs, but aligning with parseUUID keeps write-path policy uniform:
bad UUID → clean Go error + skip, not silent uuid.Nil write.
The EmbedEntity helper ran UPDATE kg_entities SET embedding = $1
WHERE id = parseUUIDOrNil(entityID). A bad UUID would resolve to
uuid.Nil and the UPDATE would silently match no rows. Today's only
caller passes a freshly-minted UUID, but swapping to parseUUID with
an explicit Warn log closes the latent trap and aligns with the
hardening policy (writes must fail fast on bad input).
Router.Get's canonical double-check branch trusted any existing entry
under the canonical key without re-checking TTL. If an earlier
agent_key caller wrote the entry and the TTL expired, a later UUID-form
caller would resolve fresh, hit the canonical branch, find the stale
entry, and return it — indefinitely — because the raw-UUID key was
never the map key and the initial-miss eviction branch did nothing.
Re-check TTL inside the double-check branch and evict+rewrite when
stale. Regression test primes a canonical entry with cachedAt set
2×TTL in the past, then asserts a UUID-form Get re-invokes the
resolver and the returned agent reflects the fresh resolver output.
The v3 workspace resolver block in injectContext was passing
l.agentUUID.String() as ResolveParams.AgentID, but the resolver uses
this value as a filesystem path segment. The v2 path in
loop_pipeline_callbacks.go and the session_key anchor both use
agent_key, so a UUID-based v3 path would produce a parallel filesystem
for the same logical agent the moment any consumer reads the v3
workspace context.
No active bug today because workspace.FromContext has zero consumers,
but this closes the landmine before the v3 wiring lands.
Rewrite inline comments added during the agent identity hardening so
they explain the code as it stands today, rather than tying to internal
plan terminology (phase numbers, FR/NFR/H/M/C codes, PR references,
trap zone labels). Commit history already carries the plan archaeology.
Comments now keep the non-obvious invariants (cache boundaries, bypass
gaps, silent-nil traps, dual-tenant semantics) and drop the scaffolding.
Comment-only — no runtime behavior change.
Clarify the agent_key vs UUID contract inline at the four highest-ROI
trap zones: the Loop struct identity fields, DomainEvent identity
fields, the parseUUID/parseUUIDOrNil store helpers, and the WS method
resolver helpers. Each comment explains the invariant and links to
docs/agent-identity-conventions.md so future readers can find the full
rules without archaeology. Comments only — no runtime behavior change.
Distills the dual-identity rules from the agent identity hardening work
into a single permanent reference. Covers all 5 trap zones with post-
hardening status, batch fail-fast contract change, router cache
canonicalization, FK safety net, telemetry type-layer defense, and
dual-tenant agent_key semantics. Adds a CLAUDE.md pointer under Key
Patterns so contributors find it via the standard onboarding path.
Three fixes required to run the integration suite after Phase 4:
1. v3_vault_store_test.go makeVaultDoc: AgentID is *string (nullable),
not string — was a pre-existing type error from v3 commit 8f56ddaa
that blocked the test binary build. Pass &agentID.
2. vault_classify_test.go imports: remove unused `store/pg` import —
pre-existing from v3 commit 4b7a2e6e.
3. TestVaultClassify_DeleteDocLinksByTypes_MultipleTypes: fix off-by-one
in target-doc loop. Pre-existing runtime panic from v3 commit — loop
created 7 targets but linkTypes slice has 8 entries, accessing docs[8]
on a len-8 slice. Changed loop to `range 8`.
4. TestVaultClassify_DeleteDocLinksByTypes_NonExistentDoc: swap
"fake-doc-id-12345" placeholder for a well-formed but unused UUID.
Post-Phase-4, parseUUID rejects bare strings at the caller boundary;
the "non-existent = no-op" contract requires a syntactically valid
UUID. Per Q7 documented contract change.
Phase 4 Step 4g integration gate.
Final step of Phase 4: rename the silently-nil-on-error helper so its
behavior is self-documenting at the call site. No behavior change — all
remaining call sites (~24) were already classified as WARN-acceptable
(read-only SELECT WHERE paths with validated inputs from cursor
pagination or WS boundary) during sub-step 4b migration.
The parseUUID error variant stays as-is for CRITICAL writes.
Also cleans up a now-stale comment in vault_handler_upload.go.
Phase 4 Step 4d of agent identity hardening (TD-1).
All 16 sites migrated. Covers BatchCreateLinks (2 sites in loop with
fail-fast per Q7), CreateLink, DeleteLink, GetOutLinks, GetOutLinksBatch,
GetBacklinks, DeleteDocLinks, DeleteDocLinksByType, DeleteDocLinksByTypes.
BatchCreateLinks previously silently dropped bad UUIDs via loop-over-
validated-docs; post-P4 the parse layer rejects the entire batch on
first bad doc_id — a documented contract change per Q7.
Phase 4 Step 4b.7 of agent identity hardening (TD-1).
CRITICAL sites migrated in the following methods (9 total):
- GetDocument, GetDocumentByID, GetDocumentsByIDs, GetDocumentByBasename
- DeleteDocument, ListDocuments, CountDocuments, UpdateHash
UpsertDocument and Search were migrated earlier as part of the
optAgentUUID wrapper fix (commit 6f6b52fb).
4 sites remain in the private helpers appendTeamFilter and
buildSearchTeamFilter — these are WARN-acceptable (team IDs from opts
structs are already validated at WS boundary). They will rename to
parseUUIDOrNil in sub-step 4d.
Phase 4 Step 4b.1 of agent identity hardening (TD-1).
5 CRITICAL sites migrated from mustParseUUID to parseUUID with wrapped
errors: GetDocument, PutDocument, DeleteDocument, ListDocuments,
IndexDocument. All handle bad agent_id input at the caller boundary
instead of hitting FK violation (writes) or returning empty results
(reads).
Scout classified GetDocument as both CRITICAL and WARN — migrating all
5 is safer and consistent (M8 note).
Phase 4 Step 4b.2 of agent identity hardening (TD-1).
optAgentUUID now returns (*uuid.UUID, error). All three known callers
updated to check the error and wrap with context:
1. vault_documents.go UpsertDocument — also migrates tenant_id and
team_id to parseUUID in the same hot path.
2. vault_documents.go Search — migrates tenant_id and agent_id.
3. vault_documents_enrichment.go FindSimilarDocs — migrates tenant_id,
agent_id, and doc_id.
Pre-Phase-4, the wrapper silently converted garbage agent_id strings to
uuid.Nil, letting the downstream INSERT hit a FK violation with cryptic
PG code 23503. Now bad input fails at the caller boundary with a clear
Go error wrapping the raw value.
Phase 4 Step 4e of agent identity hardening (TD-1).
Covers sub-step 4b migrations for vault_documents.go and
vault_documents_enrichment.go as a side effect.
Two new helpers alongside existing mustParseUUID:
- parseUUID(s) (uuid.UUID, error) — use for INSERT/UPDATE/UPSERT/DELETE
and SELECT-WHERE paths where silent nil hides bugs. Returns a wrapped
error that includes the raw value (for debuggability) and the
underlying uuid parse error (for errors.Is unwrap).
- parseUUIDOrNil(s) uuid.UUID — intentionally silent, for read-only
SELECT-WHERE paths where empty result is acceptable.
mustParseUUID is kept as a transitional alias delegating to parseUUIDOrNil
so existing call sites compile unchanged. Sub-step 4d will rename it.
Phase 4 Step 4a of agent identity hardening (TD-1).
Two teams handlers migrated to resolveAgentUUIDCached:
- handleRemoveMember (params.AgentID) accepts agent_key or UUID.
- handleTaskAssign (params.AgentID) accepts agent_key or UUID.
handleAddMember already used resolveAgentInfo, but leaked raw store
errors via "agent: " + err.Error() on resolution failure. H10 fix
replaces the leak with i18n.MsgInvalidID — no more store schema info
surfacing to WS clients.
Phase 3 of agent identity hardening.
NFR-2 exception (Phase 3 C2): ChannelInstancesMethods struct gains an
agentStore field, required to support agent_key input via the cache-aware
resolver. Constructor signature expanded and single caller at
cmd/gateway_channels_setup.go:146 updated.
Router is passed nil — channel_instances methods are not wired to the
router today, so falls back to pure DB lookup. Acceptable given create
is a rare operation. Preserves the existing i18n key MsgInvalidID.
Phase 3 of agent identity hardening.
All 3 handlers (handleList, handleGrant, handleRevoke) now accept either
a UUID or an agent_key for params.agentId via resolveAgentUUIDCached.
Adds ConfigPermissionsMethods.SetAgentRouter setter wired from
cmd/gateway_methods.go (already staged in previous commit).
Phase 3 of agent identity hardening.
All 7 heartbeat handlers (get/set/toggle/test/logs/checklistGet/checklistSet)
now accept either a UUID or an agent_key for params.agentId. Behavior
additive — existing UUID-only clients keep working.
Adds HeartbeatMethods.SetAgentRouter setter wired from cmd/gateway_methods.go
so the fast path avoids a DB roundtrip for cached agents. Preserves the
literal "invalid agentId" error string (no i18n key change).
Phase 3 of agent identity hardening.
Three-part addition so WS handlers can accept agent_key input without a
DB roundtrip on every request:
1. Router.GetCached — lock-free TTL cache read, no resolver fallback.
2. Loop.UUID() — canonical UUID accessor (agent_key stays on ID()).
3. resolveAgentUUIDCached — fast-path checks router cache via an
agentUUIDProvider interface, falls back to resolveAgentUUID on miss
or when input is a UUID string (un-cached post-canonicalization).
Helper delegates to pure DB lookup when router is nil, so handlers with
no router dependency still work. Tests cover nil-router, cache-miss, and
UUID-input paths — cache-hit coverage is exercised via downstream handler
integration tests.
Phase 3 foundation (TD-2).
Post-Phase-2 canonicalization, router cache entries are always keyed as
tenantID:agent_key. The belt-and-suspenders m.agents.InvalidateAgent(ag.ID.String())
call never matched any entry — exact-segment match on the final ":"-
delimited segment cannot match a UUID as the agentKey.
Removes the dead call and the misleading comment that justified it.
Phase 2 follow-up (H7).
Three related fixes to agent router identity handling:
1. Canonicalize on resolve — Router.Get now stores entries under the
canonical tenantID:agent_key after a successful resolver call,
regardless of whether the caller passed a UUID or agent_key. Prevents
fragmentation where the same logical agent would occupy two cache
slots. Callers that pass the UUID form are now un-cached and resolve
on every call; all production callers pass agent_key today.
2. Exact-segment match — matchAgentCacheKey helper replaces HasSuffix in
Router.Remove and Router.InvalidateAgent. Prevents substring collisions
like "tenantX:sub-foo" being wiped when invalidating "foo". Rejects
empty agentKey to guard against wildcard wipes.
3. Tenant-scoped IsRunning (C6) — Router.IsRunning now accepts ctx and
uses agentCacheKey for lookup. Pre-fix, the bare r.agents[agentID]
lookup always returned false in tenant-scoped deployments, causing
agents.list to report every live agent as idle.
Updates the sole caller at gateway/methods/agents.go:134.
Phase 2 of agent identity hardening (TD-3).
validateTeamMembership short-circuits on owner role and nil teamAccess
(lite edition), leaving downstream mustParseUUID calls as a silent-nil
trap. Validate UUIDs at the HTTP boundary — before workspace resolution,
store upsert, or event publish — so bad form input is rejected for every
caller regardless of role or edition.
Closes the owner/lite gap identified in red-team H9.
Phase 1 Fix B of agent identity hardening.
memoryflush.go constructed SystemPromptConfig without AgentUUID, while
loop_history.go set both AgentID (agent_key) and AgentUUID. buildRuntimeSection
renders below the cache boundary so the omission did not bust the prompt
cache — but the runtime identity line dropped the UUID during compaction
flush turns.
Extract a pure helper buildMemoryFlushPromptConfig so tests can assert the
config shape without a full Loop fixture (M7 mitigation from red-team
review).
Phase 1 Fix A of agent identity hardening.
Publish-time observer warns (does not block) when DomainEvent.AgentID is
non-empty and fails uuid.Parse. Distinct log field `non_uuid_agent_id`
avoids colliding with existing telemetry that parses `agent_id` as UUID.
Safety net for PR #826-class regressions. Validator lives in bus.Publish
so all call sites are covered without per-publisher instrumentation.
Phase 1 Fix C of agent identity hardening.
* fix(backup): detect pg server major for version-aware pg_dump hints
pg_dump aborts when its major version is older than the server's, so
backup preflight now runs SHOW server_version_num against the live PG
server and uses the detected major to drive:
- the missing-pg_dump hint (names the exact postgresqlNN-client)
- a new compat check that flags an installed-but-too-old pg_dump as
not-ready, instead of letting backup fail partway through the dump
Adds ParsePgDumpMajor helper for Debian/Homebrew/EDB output shapes,
covered by table-driven unit tests. Normalizes nil ctx once at
RunPreflight boundary so downstream exec.CommandContext calls are safe.
Stubs detectPGServerMajor + checkPgDumpServerCompat for the sqliteonly
build.
UI: removes the duplicate static hint from the preflight alert box so
the dynamic, actionable warning is the single source of truth. Drops
the obsolete pgDumpHint i18n key from en/vi/zh locale files.
Refs #829
* fix(docker): bump runtime base alpine 3.22 → 3.23
Alpine 3.23 main ships postgresql16-client, postgresql17-client, and
postgresql18-client simultaneously. Alpine 3.22 only shipped up to
postgresql17-client, so the backup preflight's dynamic hint to install
postgresql18-client previously failed with "no such package" on PG 18
deployments.
No client package is pre-bundled: the on-demand install via the
Packages page now resolves for any supported PG major.
Refs #829