Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,26 @@ may change in any release. See the status note in the README.

## [Unreleased]

The chat's suggestion warm-up stops spending the chat's day on a chat nobody opens, recovers from a failed or
unreachable model within minutes, and the pipeline no longer imports the chat. No migration, and no value to set.

### Changed

- **The warm-up waits for the chat to be used, and for the changes to pause.** Before the first open since start, a
run's end or a status change prepares nothing; after it, the warm-up starts once `leadgen.chat.suggestion-quiet-window`
(default 30 s) has passed without a further change, so triaging applications one by one costs one call from the
chat's day per pause rather than one per move. The first open after a start, and an open inside the window after a
change, show the catalog's sentences and phrase in the background as any miss does.
- **A failed phrasing is asked once more after a few minutes.** A call that failed, or a chat model that is named but
cannot be built, is left alone for `leadgen.chat.suggestion-retry-after` (default 5 min) instead of until the data
moved, so a local model's cold start no longer leaves the catalog's sentences up until the next run.
- **A miss is phrased in its own language.** Each miss hands over the job it built, so a German miss followed by an
English open is phrased once in each language, and the warm-up no longer reads the data a second time.
- **Follow-ups are no longer stored.** A finished turn's follow-up phrasings stay in memory, so they no longer push the
suggestions out of `chat_suggestion_phrasing`'s 256 rows; rows stored by 0.5.6 age out.
- **The data-change event moved out of the chat.** `chat.suggest.SuggestionDataChanged` is now `event.DataChanged`;
`analytics`, `application` and `archive` import nothing of the chat, and `ChatIsolationTest` checks imports and every
`chat_*` table the migrations create.
- **Spring AI 2.0.2, with openai-java held at 4.65.0.** 2.0.2 brings openai-java 4.80.0, and from 4.66.0 on a stopped
turn or one past its deadline leaves the model call running (openai/openai-java#1027; 4.96.0 still does). The hold
moves from Spring AI to openai-java, so Spring AI's own fixes land: Anthropic streaming chunks carry the thinking
Expand All @@ -29,6 +47,12 @@ may change in any release. See the status note in the README.
2.98.0 for a commit of its own; Gradle 9.9.0 waits for its `gradle:9.9.0-jdk25` image. openai-java 4.101.0 still
fails the four chat-stop tests, so the hold at 4.65.0 stays.

### Fixed

- **A run finishing during shutdown is no longer logged as not recorded.** The warm-up runs on Boot's
`applicationTaskExecutor` instead of an executor of its own, and a data change after shutdown is dropped instead of
throwing back into the writer.

## [0.5.7] — 2026-10-31

A maintenance release: dependency updates since 0.5.6, one security fix in the frontend, and a test flake fixed.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,8 @@
import de.codeministry.leadgen.chat.suggest.FollowUp;
import de.codeministry.leadgen.chat.suggest.Suggestion;
import de.codeministry.leadgen.chat.suggest.SuggestionCandidate;
import de.codeministry.leadgen.chat.suggest.SuggestionDataChanged;
import de.codeministry.leadgen.chat.suggest.SuggestionScope;
import de.codeministry.leadgen.chat.suggest.SuggestionThresholds;
import de.codeministry.leadgen.chat.suggest.SuggestionWarmup;
import de.codeministry.leadgen.chat.tools.ApplicationTool;
import de.codeministry.leadgen.chat.tools.OfferSearchTool;
import de.codeministry.leadgen.chat.tools.PinnedAdvert;
Expand Down Expand Up @@ -171,10 +169,8 @@ private static void registerChatRecords(RuntimeHints hints) {
FollowUp.class,
Suggestion.class,
SuggestionCandidate.class,
SuggestionDataChanged.class,
SuggestionScope.class,
SuggestionThresholds.class,
SuggestionWarmup.Job.class,
NewConversation.class,
ModelPick.class,
NewTurn.class,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,14 @@
* Entry point. Scheduling is enabled here because the pipeline runs inside this
* process (concept § 10, phase 1) — there is no separate worker container.
*
* <p>Asynchronous execution is enabled for exactly one thing: {@code PackageWorker}, which
* writes and removes package folders after the transaction that asked for it has committed.
* The work is disk and templates rather than a request anybody is waiting on, and Boot's own
* <p>Asynchronous execution is enabled for {@code PackageWorker}, which writes and removes
* package folders after the transaction that asked for it has committed. The work is disk and
* templates rather than a request anybody is waiting on, and Boot's own
* {@code applicationTaskExecutor} runs it — a second pool for one listener would be one more
* thing to size and shut down.
* thing to size and shut down. The chat's {@code SuggestionWarmup} runs on the same executor,
* one model call at a time and for up to the model's timeout; size or bound it with both in mind.
* Its quiet window is timed by the JDK's delay timer, not by Boot's {@code taskScheduler}, whose one
* thread a scheduled ingest pass holds for as long as it runs.
*/
@SpringBootApplication
// Named rather than scanned: a `@WebMvcTest` slice filters scanned records out but keeps
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
*/
package de.codeministry.leadgen.analytics;

import de.codeministry.leadgen.chat.suggest.SuggestionDataChanged;
import de.codeministry.leadgen.config.ConfigRegistry;
import de.codeministry.leadgen.event.DataChanged;
import de.codeministry.leadgen.filter.FilterStage;
import de.codeministry.leadgen.ingest.IngestReport;
import de.codeministry.leadgen.score.Judges;
Expand Down Expand Up @@ -323,7 +323,7 @@ private void write(
.update();
}
// The counts the empty chat suggests from settle here, not at the start (ISC-529).
events.publishEvent(new SuggestionDataChanged("run " + status));
events.publishEvent(new DataChanged("run " + status));
} catch (RuntimeException e) {
log.error("The run was not recorded: {}", e.getMessage(), e);
}
Expand All @@ -347,7 +347,7 @@ public void complete(int scored, int packaged, boolean digestWritten) {
// complete, and a run recorded before this table existed has no row.
log.debug("No run was waiting for a batch, so nothing was completed");
} else {
events.publishEvent(new SuggestionDataChanged("run completed"));
events.publishEvent(new DataChanged("run completed"));
}
} catch (RuntimeException e) {
log.error("The batched run was not completed in the history: {}", e.getMessage(), e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
*/
package de.codeministry.leadgen.application;

import de.codeministry.leadgen.chat.suggest.SuggestionDataChanged;
import de.codeministry.leadgen.event.DataChanged;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDate;
Expand Down Expand Up @@ -174,7 +174,7 @@ private static ApplicationView view(ResultSet rs, int row) throws SQLException {
public OpenReport openShortlisted() {
int opened = jdbc.sql(OPEN_SHORTLISTED).update();
if (opened > 0) {
events.publishEvent(new SuggestionDataChanged("applications opened"));
events.publishEvent(new DataChanged("applications opened"));
}
int standing = jdbc.sql(SHORTLIST_STANDING).query(Integer.class).single();
log.info("Board: {} opened at NEW, {} offers on the shortlist", opened, standing);
Expand Down Expand Up @@ -330,7 +330,7 @@ INSERT INTO application_event (application_id, from_status, to_status, note)
.params(applicationId, from == null ? null : from.name(), to.name(), note)
.update();
// Every status write passes here, so the empty chat's suggestions follow it (ISC-529).
events.publishEvent(new SuggestionDataChanged("application " + to.name()));
events.publishEvent(new DataChanged("application " + to.name()));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
package de.codeministry.leadgen.archive;

import de.codeministry.leadgen.application.ApplicationStatus;
import de.codeministry.leadgen.chat.suggest.SuggestionDataChanged;
import de.codeministry.leadgen.config.ConfigRegistry;
import de.codeministry.leadgen.event.DataChanged;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.Collection;
Expand Down Expand Up @@ -238,7 +238,7 @@ public ArchiveResult setArchived(Collection<Long> offerIds, boolean archived) {
int reset = jdbc.sql(RESET_TO_NEW).param(ids).update();
if (reset > 0) {
log.info("{} restored offers went back to NEW", reset);
events.publishEvent(new SuggestionDataChanged("restored to NEW"));
events.publishEvent(new DataChanged("restored to NEW"));
}
}
return result;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* Copyright 2026 Marcello Muscara (codeministry)
*
* Licensed under the Apache License, Version 2.0. You may obtain a copy of the
* License at http://www.apache.org/licenses/LICENSE-2.0
*/
package de.codeministry.leadgen.chat;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* A record of the chat that stays inside the process: it is handed from one bean to another and
* never reaches Jackson, so no runtime hint is kept for it (ISC-542).
*
* <p>Every other record under {@code chat} is read or written as JSON by reflection, and
* {@code LeadGenRuntimeHintsTest} fails for one without a hint. This marker is the one way out of
* that rule, and it says so where the type is declared rather than in a list inside the test: a
* record that later goes into a response or a stored column must lose the marker and gain a hint.
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface InProcess {}
Loading
Loading