3665 Commits
Author SHA1 Message Date
pnoker 929761d132 chore(db): resync schema fingerprint pins after the agentic permission seed
The agentic permission seed changed the initdb bytes, so the canonical
hash moved from 0d9ad80328ef1a6ba91e53cf0c6db25812b8bd236e280939f967beee687d5108
to 78692f01fac7814f6a225ff87532502536a23972bf05dd0ab8768e629be19dc8;
resync the embedded ddl_hash and the compose/env pins with
schema_fingerprint.py --sync. Databases initialized before the seed
converge via migrations/05-agentic-permission-resources.sql plus a
public.dc3_schema_fingerprint ddl_hash update at cutover.
2026-10-24 02:04:27 +08:00
pnoker f007925da4 fix(driver): register leshan default models as native image resources
leshan-core's ObjectLoader.loadAllDefault streams its default OMA model
DDFs from models/*.xml inside the jar, but the jar ships no native-image
configuration of its own; dropped from the image, the lwm2m driver died
at boot with FileNotFoundException /models/0-1_0.xml and had been
crash-looping since the 2026-10-16 deploy. Register a models/* glob in
DriverNativeRuntimeHints; resource hints are globs, not regexes, in
Spring Boot 4's unified reachability-metadata.json.
2026-10-24 02:04:18 +08:00
pnoker 33f6180e28 fix(agentic): register caffeine policy classes for native images
The native profile passes -H:ConfigurationFileDirectories, which
disables classpath scanning of jars' bundled native-image configs, so
the caffeine policy family that the new chat/connectivity caches
resolve at runtime must be registered through Spring AOT hints; without
them the agentic center crash-looped on native boot with
ClassNotFoundException SSMSA. Copy the registration pattern from
DataNativeRuntimeHints and wire the registrar through
META-INF/spring/aot.factories.
2026-10-24 02:04:02 +08:00
pnoker 65330ed23c fix(agentic): name the agentic message service bean explicitly
The agentic MessageServiceImpl kept Spring's default bean name, which
collides with dc3-common-data's same-named class once dc3-center-single
component-scans both modules: the AOT context (and plain JVM startup)
dies with ConflictingBeanDefinitionException. Follow the
dataDashboardService precedent and pin an explicit module-prefixed name.
2026-10-24 02:03:53 +08:00
pnokerandClaude Code 4ec088f174 chore: drop the agentic audit report from the repo
The audit report was a working artifact for the hardening campaign; the
fixes and their tests carry the knowledge now.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-22 09:25:51 +08:00
pnokerandClaude Code b11e19c936 docs(agentic): record the gap-sweep round in the audit report
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-22 02:28:06 +08:00
pnokerandClaude Code 75bbad54fb style(agentic): apply spotless across the campaign modules
Includes the named-column fix for the message count query (the
positional row.get(0) broke on the scripted test harness).

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-22 01:07:12 +08:00
pnoker 47c359033d merge residual transport, storage and frontend gaps 2026-10-22 01:02:52 +08:00
pnokerandClaude Code e7736a24c8 fix(auth): resync the frozen agentic openapi snapshot and guard it
- flatten the provider routes in openapi-agentic.json
  (/provider/config/{add,update,delete} -> /provider/{add,update,delete})
- move /message/list and /action/pending to POST bodies
  (ConversationPageQueryDTO) with OffsetPage envelopes; add the
  OffsetPageMessageVO schema
- add the missing /attachment/delete endpoint (query id, 204)
- document /chat/completions responses with ChatCompletionResponseVO /
  ChatCompletionChunkVO schemas, the finish_reason enum (with the
  max_rounds/error/cancelled platform extensions) and SSE event naming
  (chat.completion.chunk, agentic.event, agentic.visualization, [DONE],
  keep-alive heartbeat)
- switch /attachment/upload to a multipart/form-data file part
- trim SessionVO (tenantId/userId) and AttachmentVO (filePath); extend
  SessionExt with icon/category/archived; fix MessageVO.status to
  COMPLETED/FAILED/CANCELLED
- add the 11 lastCheck* health-profile fields to ModelProviderVO /
  ModelConfigVO and fix the NOT_DEFAULT example
- add AgenticOpenApiSnapshotGuardTest: reflects the dc3-common-agentic
  controllers and fails the build on snapshot route drift (test-scoped
  dependency on dc3-common-agentic; allowlist covers the x-dc3-ai hidden
  provider/model checks and is itself verified against the annotations)
- align the e2e fixture provider routes, the live-harness message/list
  call (POST body + OffsetPage unwrap, throw on non-200), and the mock
  layer (OpenAI non-stream envelope, SSE event lines + keep-alive
  comments, unreachable chat dispatch handler removed, conversation_id
  required validation mirroring the server's 422)
- drop the deleted AgenticToolMetadata entry from the center-agentic
  native reachability metadata

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-22 00:57:37 +08:00
pnokerandClaude Code 97090d5369 fix(agentic): close the residual hardening gaps in transport and storage
1. acquireInFlightSlot: register a doOnDiscard hook so a permit granted
   to a cancelled subscriber (tryAcquire won, emission refused) is
   returned instead of leaking the slot; verified against reactor-core's
   MonoCallable discard path and covered by a cancel-race test.
2. evict(): keep the provider in-flight semaphores - the key space is
   bounded by database (tenant, provider) rows, while removing one with
   outstanding leases would over-admit on rebuild and orphan the
   registered permit gauge.
3. listOptions(): stop advertising the fallback model while
   transport-fallback-enabled is off (the default); selecting it 503s.
4. Attachment quotas: ReactiveAttachmentStore gains countByTenant and
   sumSizeByTenant (tenant-wide, soft-deleted rows excluded); upload
   enforces attachment-tenant-max-count / attachment-tenant-max-bytes
   after the whitelist check and rejects with the tenant's current
   usage before a single byte is written.
5. Web 401 convergence: extract handleAuthFailure (aggregated warning
   toast, auth-session reset, router redirect) shared by the axios
   response interceptor and the fetch-based agentic stream path - no
   raw window.location.hash jumps.
6. Dead code removal (all zero references): AgenticConstant
   MAX_AGENT_LOOP_ROUNDS, R2dbcModelProviderStore utc()/utcNow(), the
   unused Mono import in ReactiveAgenticToolCallback, InsightTool
   DOMAIN.
7. Documentation hygiene: README component table lists only real
   classes (adds InsightTool/HistoryTool, the registry as the single
   registration path); MessageServiceImpl javadoc reflects the
   pushed-down pagination; CommandTool/PointValueTool javadoc name the
   tool registry instead of Spring AI @Tool.
8. Property samples: application-agentic.yml and the root .env.example
   carry the security master key, transport limiting, cache and
   attachment quota knobs with production guidance (master key must be
   set, fallback stays off, private base URLs are rejected).
9. Attachment ids are numeric end to end: ChatCompletionRequest and
   message contentExt use number[] (server List<Long>); the store sends
   raw ids with legacy numeric-string coercion on retry/history; the
   mock seeds and upload handler return numeric ids.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-22 00:56:58 +08:00
pnokerandClaude Code 68dca05184 feat(agentic): expose a dns pinning seam for probe clients
AgenticBaseUrlValidator.pinnedDns resolves and validates the host now
and returns a resolver fixed to those addresses, so probe clients can
close the DNS-rebinding window between validation and connect. The
functional shape (host -> validated addresses) keeps the module free of
HTTP-client dependencies; wiring it into the OkHttp clients of both
connectivity checkers lands with the transport factory consolidation.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-22 00:56:32 +08:00
pnokerandClaude Code 7f65302280 feat(agentic): add a soft context token budget for replayed memory
dc3.agentic.context-token-budget (default off) bounds the assembled
prompt: when the estimated user message plus memory turns exceed the
budget the oldest turns are dropped until it fits, so long sessions no
longer rely solely on the provider-side overflow error. Tool schemas
and tool results stay bounded by the runtime truncation cap instead.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-22 00:42:12 +08:00
pnokerandClaude Code f8708428e5 fix(agentic): cache provider tool formats and break stuck tool loops
The provider-format tool list and callback map were rebuilt and the
schema JSON re-parsed on every round of every request; both are now
cached against the immutable registry snapshot. The agent loop
additionally tracks identical (tool, arguments) pairs per turn: from the
third repetition the tool still executes but its result tells the model
to change course, so stuck loops self-correct instead of burning the
round budget. Regression tests cover the backpressure guard's
budget/grace/reset semantics.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-22 00:39:09 +08:00
pnokerandClaude Code 32063ee016 feat(agentic): enforce https base urls and add tool-level metrics
Two audit residuals: the base URL validator now rejects plain http by
default (dc3.agentic.security-base-url-https-only, opt-out for trusted
plain-http deployments) and blocks NAT64 64:ff9b::/96, 6to4 2002::/16
and IPv4-mapped addresses that could tunnel past the IPv4 rules; tool
tracing now records agentic.tool.calls{tool,domain,outcome} counters and
agentic.tool.duration timers through the registry's optional meter
registry, closing the last missing observability group.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-22 00:29:09 +08:00
pnokerandClaude Code 164a10ffb6 feat(agentic): propagate provider token usage end to end
The runtime captured usage but nothing consumed it. Usage now flows from
both runtimes (stream terminal frames carry it; the Spring AI path maps
ChatResponse metadata) through AgenticChatServiceImpl into: the blocking
response VO (estimated=false), the stream terminal chunk (include_usage
semantics, estimates never hit the wire), the persisted message tokens
(new estimated flag distinguishes reported from estimated), and a new
agentic.chat.tokens counter. Meter model labels are bounded so
user-supplied model names cannot explode registry cardinality.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-22 00:16:03 +08:00
pnokerandClaude Code aba4e752c8 fix(agentic): require a fresh api key when re-pointing a provider
The update path kept the stored (write-only) API key whenever the
request omitted one — including when the base URL changed, so a
compromised tenant session could silently redirect the unreadable
stored secret to an arbitrary public host. The update path now enforces
the same rule as the connectivity probe: a changed base URL without an
explicit new key is rejected.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-21 23:53:22 +08:00
pnokerandClaude Code 0866412fb8 fix(agentic): adapt the db-tck session store contract test to the new constructor
The store layering fix removed AttachmentFileCleaner from the R2DBC
session store constructor; the dialect contract test still wired the
old signature and only compiled in a full-reactor build.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-21 09:54:54 +08:00
pnokerandClaude Code 8740a5cd12 style(agentic): use slf4j placeholders in the new hardening logs
The repo-wide logging policy gate rejects message concatenation; the
five call sites introduced by the hardening campaign are folded into
single placeholder templates.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-21 09:40:12 +08:00
pnokerandClaude Code 9c092e2e26 fix(agentic): push message pagination down to the store
The message list endpoint materialized the full conversation and sliced
in memory. The port gains a paged list default (adapters are encouraged
to override with LIMIT/OFFSET) plus a count query; the R2DBC adapter
overrides both and MessageServiceImpl assembles the page from a zipped
count+page instead of collecting the whole conversation.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-21 01:48:37 +08:00
pnokerandClaude Code 24fc40056f fix(agentic): bind provider store timestamps through the r2dbc dialect
Follow-up to the store workstream: R2dbcModelProviderStore kept the
UTC-naive LocalDateTime bindings, so wall-clock columns drifted with the
database session timezone. All time bindings now go through
R2dbcDialect.bindInstant like the other agentic stores.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-21 01:41:28 +08:00
pnoker a76d06925c merge store-layer concurrency and layering fixes
# Conflicts:
#	dc3-db/dc3-db-agentic/pom.xml
2026-10-21 01:21:35 +08:00
pnokerandClaude Code f9147d74c0 fix(agentic): harden attachment upload intake
Close the gap the security workstream left unplugged: uploads now pass
an extension whitelist before any byte hits disk, the persisted file
name is normalized (control characters stripped, length capped with the
extension preserved), the storage path segment is bounded so a long name
cannot blow the filesystem limit, and channel open/close run on bounded
elastic instead of the event loop. Regression tests cover the whitelist
rejection and the name normalization contract.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-21 01:20:29 +08:00
pnokerandClaude Code 20cf63ef61 fix(agentic): make the store layer concurrent-safe and layered
M21 (soft-delete tombstone poisons conversationId): touch() hit the
full-table unique constraint uq_session_tenant_user_conversation when a
soft-deleted row still held the natural key and surfaced the raw
DataIntegrityViolationException. The constraint cannot become a partial
unique index: dc3_message, dc3_attachment, and dc3_action reference it
with foreign keys, and PostgreSQL FK targets must be a non-partial
unique constraint/index. Product semantics are implemented in the write
path instead: touch() now locks the row unfiltered (SELECT ... FOR
UPDATE without a deleted filter), revives a tombstone as a brand-new
session (deleted=0, title and session_ext reset, creator and create_time
restarted), and retries the whole decision once when a concurrent
insert wins the unique-key race. No DDL change, no fingerprint churn.

M22 (session_ext lost update): update() read-merged-wrote without a
lock or transaction, so concurrent renames overwrote each other. It now
runs lock-merge-update inside one transaction, reusing touch()'s row
lock, so concurrent preference changes merge.

L51 (attachment save TOCTOU): R2dbcAttachmentStore.save inserted without
checking the session, so a save racing a concurrent session delete
survived as a live attachment row of a soft-deleted conversation. save
now takes the message store's live-session guard (SELECT id FROM
dc3_session ... deleted = 0 FOR UPDATE) in the same transaction, and
the session delete locks the session row before soft-deleting children,
so upload and delete serialize: the upload either lands before the
delete or fails its guard. File cleanup is collected after commit from
the new listDeletedFilePaths port query (soft-deleted rows only, so a
revived conversation keeps its new files).

M17 (layering inversion): R2dbcSessionStore injected the service-layer
AttachmentFileCleaner and performed filesystem unlinking from the
persistence adapter. The orchestration moved to SessionServiceImpl:
the store is a pure SQL adapter again, the service reads the deleted
attachment paths after the delete commits and unlinks best-effort.
A layering guard test keeps service.impl imports out of the db module.

M23 (blocking file IO on the event loop): AttachmentFileCleaner ran
Files.deleteIfExists on the caller's thread, which is the Reactor
Netty event loop after R2DBC completions. The cleaner now owns the
offload: every unlink/unlinkAll subscribes on bounded elastic, with a
documented exception in the r2dbc migration gate for the one
boundedElastic reference. AttachmentServiceImpl's open/close paths are
left for the security theater to align.

M24 (timezone drift): the agentic stores bound UTC-naive LocalDateTime
to TIMESTAMPTZ columns, which drifts whenever the database session
timezone is not UTC. All timestamp binds across R2dbcSessionStore,
R2dbcMessageStore, R2dbcActionStore, R2dbcAttachmentStore, and
R2dbcModelConfigStore now go through dialect.bindInstant (Instant, and
Instant-typed nulls), matching the platform convention; BO wall-clock
fields stay UTC-naive and reads normalize via DatabaseInstant. The same
pattern remains in R2dbcModelProviderStore (owned by another theater).

Regression coverage: a scripted in-memory DatabaseClient models READ
COMMITTED row locking and unique-key conflicts, with tests for
tombstone revive, concurrent-update merge without lost writes, the
delete-vs-upload TOCTOU, guard ordering, Instant binds, and non-UTC
offset decode normalization. dc3-db-agentic gains its first test suite
(26 tests); dc3-common-agentic keeps 206 green.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-21 01:13:06 +08:00
pnokerandClaude Code eeaa861328 fix(agentic): align mock engine and cli with the revised wire contract
Follow-up to the contract workstream: the mock handlers, the frontend
chat request type (conversationId now optional), the mock-adapter test
and the dc3-cli provider/session commands move to the new routes —
provider /add|update|delete without the config nesting, POST body
pagination for message/list and action/pending.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-21 01:10:36 +08:00
pnoker 65d39004c3 merge chat wire contract alignment and permission closure 2026-10-21 00:55:24 +08:00
pnoker cd939f92a4 merge provider security and observability hardening 2026-10-21 00:52:41 +08:00
pnokerandClaude Code 3f041fb4bd fix(agentic)!: align the chat wire contract and close the permission model
- H7 (OpenAI wire divergence): the non-streaming completion response serialized
  finishReason/promptTokens/completionTokens/totalTokens in camelCase while the
  chunk VO and the frontend contract already declared snake_case, so clients never
  read the values. Choice.finishReason and all three Usage fields now carry
  @JsonProperty(finish_reason/prompt_tokens/completion_tokens/total_tokens); the
  request side accepts max_tokens and max_completion_tokens via @JsonAlias and the
  value is already mapped through to the provider request.
- H8 (conversationId contradiction): the OpenAPI schema said omitted ids start a
  new conversation, but the preparer rejected the request at runtime. The server
  now generates a fresh UUIDv7 conversation id when the field is omitted (OpenAI
  stateless style); an explicit id still enables memory replay.
- M29 (messages semantics): the request declared full-context messages but only
  the last user message was used. The preparer now honors the whole transcript:
  system messages merge into the system prompt after the platform prompts, the
  last user message drives the turn, and every earlier user/assistant turn is
  replayed. When the client carries prior turns it manages its own context, so
  platform memory replay is skipped for that turn (no double replay); with only
  the current turn, platform memory replays per memoryEnabled/historyWindowSize.
  Tool-role messages are accepted but not replayed (no tool-call correlation).
- M31/M55/M18 (message list): the message list endpoint bypassed the service
  layer and returned an unpaged list. Added MessageService/MessageServiceImpl,
  moved the controller to controller->service->repository layering, and switched
  /message/list to POST-body offset pagination (defaults 50, cap 200, OffsetPage
  envelope) matching the session convention. Pagination is applied in the service
  until the store port gains an offset/limit overload (storage war zone).
- M32 (pagination style split): the module mixed POST-body, GET-query and absent
  pagination. Session list keeps the platform POST-body style; /message/list and
  /action/pending now use the same POST-body + OffsetPage shape; attachment and
  model/provider catalog lists stay unpaginated as bounded per-conversation or
  config-scope sets.
- M33/L53 (filePath leak): AttachmentVO exposed the server-side storage path;
  the field is removed from the VO (the BO keeps it internally) and the frontend
  attachment type no longer expects it.
- M56 (provider route nesting): a provider is a single resource, so
  /provider/config/{add,update,delete} collapsed to /provider/{add,update,delete}
  (breaking; the frontend API layer now calls the flat routes).
- M57 (fake session fields): SessionVO carried tenantId/userId that the
  controller nulled per response; the fields are removed together with the
  sanitize workaround and a serialization guard test asserts the keys never
  reappear.
- M27/M30/M55 (permission model): verbs now follow the platform five-verb
  convention - chatCompletion: chat:add; action confirm/reject: action:update;
  action pending: action:list; attachment upload: attachment:add (breaking).
  The agentic permission codes were absent from the dc3_resource seed, so only
  wildcard admins could ever use the assistant: 21 API-type resource rows are
  seeded in initdb/02-iot-dc3-auth.sql and backfilled for existing databases by
  migrations/05-agentic-permission-resources.sql (idempotent ON CONFLICT DO
  NOTHING; entity_id -1 placeholder is adopted and relinked by the runtime
  resource registrar, which retires the pre-fix verb codes on first sync).
- H6 attachment side (prompt injection delimiting): attachment metadata enters
  the prompt wrapped in explicit UNTRUSTED_ATTACHMENT_METADATA markers with a
  data-only notice, control characters are stripped (a file name cannot forge
  markers or line structure) and per-line length is capped by
  attachmentNameMaxLength; instructions always precede the untrusted block.
- M37/M46 (SSE hardening): streaming frames now carry event names derived from
  their object discriminator (chat.completion.chunk, agentic.event,
  agentic.visualization) with [DONE] kept on the default event, and a
  keep-alive comment heartbeat (streamHeartbeatSeconds, 0 disables) is merged
  into the stream and terminates with it.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-21 00:52:32 +08:00
pnokerandClaude Code 43aeb11fea fix(agentic): harden provider security and close the observability gaps
H4 (SSRF): every user-supplied base URL now passes AgenticBaseUrlValidator
(http/https only; all resolved addresses must be public — loopback, private,
link-local, CGNAT, TEST-NET, reserved and ULA ranges rejected, DNS-rebinding
multi-answer included, verdicts cached 30s). Draft probes, stored-provider
probes (including model-config checks) and provider add/update all validate
through it, and probing a stored provider against a different base URL
requires an explicit API key so the stored key is never replayed to an
arbitrary host.

H5 (plaintext keys at rest): new AgenticKeyCipher (AES-256-GCM envelope,
enc:v1:<base64(iv||ciphertext||tag)>). R2dbcModelProviderStore encrypts on
write and decrypts on read; legacy plaintext rows pass through on read and
upgrade to ciphertext on the next write (lazy migration). Master key resolves
from dc3.agentic.security-master-key, then DC3_AGENTIC_MASTER_KEY, then a
stable HKDF-SHA256 derivation from DC3_SECURITY_KEY (warned once at startup);
decrypt failures fail loudly instead of degrading to plaintext.

M25/M38 (unbounded check caches): connectivity results move to bounded
Caffeine caches — stored results capped by check-cache-maximum-size/TTL,
draft probes (client-material keys) in a separate 32-entry partition with
half the TTL — and the in-flight merge map is capped, running probes
unmerged past the cap instead of growing.

M26/M35 (half-finished throttling): the platform-wide fallback is now
opt-in (transport-fallback-enabled, default false) — tenants without a model
configuration get a clear 503 problem instead of silently using the global
key; the fallback client is never cached. Cache-miss/unknown-model acquires
are throttled by the global semaphore, acquire waits are bounded by
transport-acquire-timeout-seconds (fast 503 on timeout, no event-loop
blocking — the wait parks on the virtual-thread slot scheduler), and chat
entries pass a per-tenant QPS token-bucket gate (transport-per-tenant-qps,
429 on overflow, 0 disables).

M19/M36 (no cache TTL): the four ChatClientFactory caches expire after
transport-cache-ttl-seconds, so out-of-band provider edits stop serving
stale transports.

H9 (zero metrics): Micrometer instrumentation — provider/global semaphore
permit gauges and acquire-timeout counter in ChatClientFactory,
agentic.chat.requests{tenant,provider,model,result} counter plus a duration
timer and a per-tenant QPS gate counter in AgenticChatServiceImpl, and an
agentic.check.results{kind,result} counter for connectivity probes.

H10 (broken trace): new RequestIdContextPropagationConfig in dc3-common-web
registers a ThreadLocalAccessor bridging the dc3.requestId Reactor context
key into the MDC (mirroring the MQ module precedent) and enables automatic
context propagation, so requestId reaches every thread a reactive chain hops
to and flows through the insight RPC chain.

L54 (insight error leakage): InsightTool failures now answer the model with
a stable, category-classified phrase (network/storage/invalid
request/unexpected) plus the correlated requestId, capped at 200 chars;
internal details only reach the server log.

L58 (logs missing tenant): chat lifecycle logs carry tenantId, userId and
durationMs (no request content, no credentials).

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-21 00:50:53 +08:00
pnoker 733a00b7ce merge front-end agentic hardening (ime, streaming pipeline, session pagination, retry) 2026-10-21 00:49:53 +08:00
pnokerandClaude Code 92074eeca1 fix(web): make the agentic assistant robust for streaming and ime input
- H11 (IME enter): the composer submitted the draft on the Enter that
  confirms an IME candidate, so CJK users could not type multi-character
  candidates. All Enter-driven actions on agentic text inputs (composer,
  header rename, rail rename) now guard event.isComposing ||
  event.keyCode === 229 via the shared isImeCompositionKey helper.
- M39 (quadratic streaming hot path): every token triggered a full
  markdown re-parse + DOMPurify pass + O(n) scroll signature rebuild.
  Deltas now coalesce in a 50ms window before hitting the store (always
  flushed synchronously on finish/failure/reset so no text is lost), the
  streaming tail renders as plain text while closed segments render from
  a memoized parse+sanitize cache, and the cache is dropped at completion
  so the final frame re-parses and re-sanitizes the whole message. The
  scroll signature is O(1): list length + tail identity/lengths only.
- M40 (mock mega-deltas): the scripted engine emitted each answer as one
  giant delta, so the corpus never exercised token streaming. It now
  slices replies into random 1-8 char deltas and splits one content frame
  across two enqueues per stream; new transport tests cover cross-read
  frame reassembly, multi-frame reads, unterminated trailing frames,
  malformed payloads, and mid-stream aborts.
- M41 (docked history dropdown fork): the dropdown mixed archived
  sessions in, had no truncation hint and no search, diverging from the
  workbench rail. Both surfaces now share one contract (sessionList
  helpers): archived folded out, one title+summary search rule, and
  pagination reachable from both.
- M42 (session list hard cap): conversations beyond the first 50-session
  page were unreachable. loadMoreSessions appends the next page (stable
  row identities, deduped, offset tracked over server rows only) and the
  rail/dropdown expose a load-more control driven by the page hasNext
  flag.
- L60 (no retry for failed turns): failed assistant replies now carry a
  regenerate action that cuts the failed turn and replays its user
  message (with the original attachment ids) through the normal
  sendMessage pipeline.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-21 00:47:46 +08:00
pnokerandClaude Code b10891422b fix(agentic): sanitize tool error messages replayed to the model
Raw exception messages could echo internal class, query and host details
into the model context and the persisted trace. Argument failures now
map to a stable phrase, everything else is length-capped; full details
stay in server-side logs.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-21 00:35:18 +08:00
pnokerandClaude Code 4a3f837178 fix(agentic): tell the model when id normalization dropped entries
normalizeIds silently capped lookups at MAX_IDS; the model received a
partial answer and treated it as complete. Every bulk-lookup tool now
appends a truncation note ("queried first N of M provided ids") to its
result message, matching the SystemTool id-list behavior from the
registry unification.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-21 00:33:16 +08:00
pnokerandClaude Code 3107e146c7 fix(agentic): collapse the tool registry into one registration path
Root-cause fixes for the audit's registry findings:

- One registration path: the tenant/user/system lookups move from the
  reflective @Tool channel into the reactive registry, so both agent
  runtimes expose the same tool surface; the MethodToolCallbackProvider
  channel, its tracing callback wrapper and the @AgenticToolMetadata
  annotation are deleted (domain is now declared explicitly at
  registration and carried through the tracing wrapper).
- The 8 verbatim-duplicated functional interfaces and factory methods
  collapse into one ToolCall contract and one tool() factory that parses
  arguments once and turns malformed JSON into an INVALID_ARGUMENT
  envelope the model can self-correct from, instead of an error.
- The three telescoping constructors are gone; the traced snapshot is
  built once at construction time and returned as the same immutable,
  order-preserving instance on every call.
- number() coerces LLM-friendly integer forms via BigDecimal
  (10.0, 1e3, "42"); non-integral or unparsable values yield null
  instead of NumberFormatException. The history bridge parses after its
  null guard now.
- Analytics schemas spell out their series/window items (mirroring the
  AnalyticsModel records) instead of bare {"type":"object"} shapes;
  numeric bounds come from ToolLimit-bound constants and the offset/
  limit fragment is assembled by a single helper.
- SystemTool id lists are capped at ToolLimit.MAX_IDS with a truncation
  note in the result message; the contract test now guards that every
  public tool method is actually registered (and vice versa).

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-21 00:29:37 +08:00
pnokerandClaude Code 90a4f35ee3 fix(agentic): unify the tool channel and make the agent loop resilient
Root-cause fixes for the audit's runtime findings:

- One tool registry, both runtimes: ChatClientConfig no longer maintains a
  separate reflective @Tool channel; the Spring AI path now consumes the
  reactive registry through ReactiveAgenticToolCallback bridges, so the
  tool surface is provider-independent (the bridge runs on bounded
  elastic, guaranteed by subscribing the whole Spring AI chain there).
- Hallucinated tool names answer with a tool-error envelope listing the
  real tools instead of killing the whole turn (H1).
- Slow SSE consumers are tolerated up to a bounded backpressure buffer
  with an idle-timeout grace period instead of the zero-demand instant
  kill (H2); AgenticBackpressureGuard carries the accounting.
- The loop budget closes turns with a forced no-tools final round
  (finish reason max_rounds) instead of hard-failing (loop policy
  configurable via dc3.agentic.loop-*, legacy behavior opt-out).
- Provider-reported token usage is captured from usage frames and
  completions and carried on AgenticRuntimeResult (M34).
- Oversized tool results are truncated inside a valid envelope instead
  of crowding out the conversation context.
- The system prompt gains an untrusted-content security section (H6);
  tool tracing domains come from explicit declarations instead of
  name-prefix heuristics.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-21 00:04:35 +08:00
pnokerandClaude Code e2c69328aa feat(agentic): add the hardening configuration surface
Frozen property contract shared by the agentic audit remediation
workstreams: agent loop termination policy, streaming backpressure
budgets, provider key encryption master key, transport limiting
timeouts/quotas, cache TTLs, connectivity cache bounds and attachment
whitelist/quota settings.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-20 23:40:02 +08:00
pnokerandClaude Code 9f18737356 feat(agentic): add the hardening configuration surface
Frozen property contract shared by the agentic audit remediation
workstreams: agent loop termination policy, streaming backpressure
budgets, provider key encryption master key, transport limiting
timeouts/quotas, cache TTLs, connectivity cache bounds and attachment
whitelist/quota settings.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-20 23:39:30 +08:00
pnokerandClaude Code 2a20165c5f feat(agentic): add the hardening configuration surface
Frozen property contract shared by the agentic audit remediation
workstreams: agent loop termination policy, streaming backpressure
budgets, provider key encryption master key, transport limiting
timeouts/quotas, cache TTLs, connectivity cache bounds and attachment
whitelist/quota settings.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-20 23:38:34 +08:00
pnokerandClaude Code 2d083acfc8 feat(agentic): add the hardening configuration surface
Frozen property contract shared by the agentic audit remediation
workstreams: agent loop termination policy, streaming backpressure
budgets, provider key encryption master key, transport limiting
timeouts/quotas, cache TTLs, connectivity cache bounds and attachment
whitelist/quota settings.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-20 23:38:06 +08:00
pnokerandClaude Code 773df65e9e feat(agentic): add the hardening configuration surface
Frozen property contract shared by the agentic audit remediation
workstreams: agent loop termination policy, streaming backpressure
budgets, provider key encryption master key, transport limiting
timeouts/quotas, cache TTLs, connectivity cache bounds and attachment
whitelist/quota settings.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-20 23:34:36 +08:00
pnokerandClaude Code 56ec00f3c8 docs(release): update generated changelog and bump to 2026.10.10
Co-Authored-By: Claude Code <noreply@anthropic.com>
v2026.10.10
2026-10-17 01:21:08 +08:00
pnokerandClaude Code 0a0ea41142 ci(native): move opc-ua off the driver native matrix after the runner oom
The opc-ua driver (Eclipse Milo + netty + jaxb universe) exhausts the
16GB hosted runner during the native-image analysis step even with
-J-Xmx10g; it builds fine on larger local machines. Its native image has
never shipped, so dropping it from the matrix removes nothing users
already have. It stays on the JVM line with dnp3/ble/opc-da until the
heap cap is property-driven or a larger runner hosts the job.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-17 01:20:24 +08:00
pnokerandClaude Code 302cef047b docs(release): update generated changelog and bump to 2026.10.10
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-17 00:30:40 +08:00
pnokerandClaude Code cebb762804 fix(native): carry agent snapshots for the mqtt and modbus-tcp drivers
Both drivers died at boot with NoClassDefFoundError on
org.slf4j.MarkerFactory: their protocol SDKs (Paho, Modbus4J via the
mangoautomation repository) exercise ServiceLoader paths that
DriverNativeRuntimeHints does not express, and unlike the rest of the
driver line they had no agent snapshot to close the gap. The snapshots
are committed per native_agent_collect.sh's stated-reason exception:
booting each repackaged JAR with the native-image-agent records the
SLF4J/logback binding metadata the closed-world analysis needs.

Verified by rebuilding both native images from scratch and running them
bare against a live broker: both subscribe on untagged dc3.q.* queues
and stay up instead of crash-looping.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-17 00:27:08 +08:00
pnokerandClaude Code 8e558e0d74 fix(native): default spring env in native runtime so broker topology stays untagged
Native binaries cannot re-resolve the spring.profiles.active
${NODE_ENV:dev} placeholder at runtime: the AOT pass freezes the dev
profile (and application-dev.yml) into the image. RabbitEnvironmentConfig
then reads spring.env=dev and prefixes every exchange and queue with the
dev_<user> isolation tag, which silently splits the broker topology
against JVM services running with NODE_ENV=test (messages end up
NO_ROUTE between the two lines).

Build-time profile pinning does not reach the AOT snapshot (verified
with NODE_ENV, SPRING_PROFILES_ACTIVE env vars and MAVEN_OPTS system
properties). SPRING_ENV feeds the same EnvironmentPostProcessor through
the system-environment property source, which outranks profile YAML at
runtime, so one ENV default in native-runtime-base covers every native
target. Deployments that want an isolation tag can override the
variable.

Verified by rebuilding dc3-driver-virtual-native and running the image
bare: subscriptions now use untagged dc3.q.* names.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-16 10:43:01 +08:00
pnokerandClaude Code 3c43e720bd fix(native): drop the duplicated swagger pin and reword the buffer comment
Two fast CI gates caught the tail of the driver native campaign: the
root pom declared swagger-annotations-jakarta twice (the pin already
existed next to the mockito pins - Maven 4 treats the duplicate
dependencyManagement entry as an error, which killed every early plain
mvn invocation on CI while local 3.9 only warned), and the r2dbc
migration gate's literal jdbc: pattern matched the hyphenated
'sqlite-jdbc:' inside a DriverNativeRuntimeHints comment. Remove the
duplicate entry and reword the comment; no gate semantics change.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-14 08:00:41 +08:00
pnokerandClaude Code b965acd5ef feat(native): register the sl651 optional dependency bridge
Sl651DriverCustomServiceImpl reaches the SL651 stack through
Class.forName and a JDK proxy over an optional dependency. Without
registration a native image silently takes the sl651ApiMissing branch:
the driver boots and reports health but never receives a message. Add a
module-level Sl651NativeRuntimeHints registering SL651Server and
ISl651MessageListener, wire it through aot.factories, and carry the
module's tracing snapshot. Functional acceptance remains a live SL651
message round-trip on real hardware.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-14 01:34:48 +08:00
pnokerandClaude Code 9832bd6b4a feat(native): onboard the jserialcomm serial driver batch
Batch 3 joins the native line: modbus-rtu, serial, mbus, dlt645, dlms
and zigbee - the jSerialComm serial family. jSerialComm ships no
native-image configuration of its own, so DriverNativeRuntimeHints
registers the embedded per-platform JNI libraries as resources and the
SerialPort JNI surface explicitly (string-based references keep
non-serial drivers unaffected). The tracing snapshots carry the early
boot surface only - serial paths open on device access and stay covered
by the explicit registrations. zigbee is best-effort: no radio hardware
locally, gRPC registration and scheduling are the acceptance line.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-14 01:34:47 +08:00
pnokerandClaude Code b73e4cfa35 feat(native): onboard the database and messaging driver batch
Batch 2 joins the native line: mysql, postgresql, sqlserver, oracle,
redis and kafka - database dialects and messaging clients, all pure
Java. DriverNativeRuntimeHints gains the four JDBC dialect driver
constructors (string-based TypeReferences; each class exists only on
its own driver's classpath and native-image ignores hints for absent
classes). Same generated shape as the previous batches: targets, matrix
entries, compose blocks and per-driver agent snapshots.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-14 01:21:49 +08:00
pnokerandClaude Code 61420025e7 feat(native): onboard the protocol sdk driver batch
Batch 1b joins the native line: knx, bacnet-ip, melsec, plcs7, lorawan,
opc-ua, lwm2m and iec61850 - pure-Java protocol SDKs (calimero,
bacnet4j, iot-communication, milo, leshan, openiec61850). iec61850's
GOOSE raw-ethernet paths stay untested on native and are validated in
TCP mode only, as flagged in the CI matrix comment. Same shape as batch
1a: native targets, matrix entries, compose overlay blocks behind the
native-drivers profile, and per-driver agent snapshots.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-14 01:15:18 +08:00
pnokerandClaude Code 691d8a6ade feat(native): onboard the pure java protocol driver batch
Batch 1a joins the native line: tcp-udp, listening-virtual, can,
ethernet-ip, fins, http, snmp, iec104 and coap - pure-Java protocol
stacks with no JNI dependencies. Native targets mirror the jar line's
directory layout (listening-virtual keeps its TCP/UDP EXPOSEs), the CI
matrix gains the nine entries, and the compose overlay defines the nine
services behind the native-drivers profile with keys matching
docker-compose.yml so overlay semantics hold (the pilot keys are renamed
to the short main-compose form for the same reason).

Each driver carries a tracing-agent snapshot per the dual-track policy:
RuntimeHints covers the shared SDK surface, snapshots carry the
logback/logstash parsing depth and per-protocol reflective access.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-14 01:01:31 +08:00