@vellumai/assistant 0.8.11 → 0.8.12-staging.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +15 -17
- package/README.md +0 -6
- package/bun.lock +6 -122
- package/node_modules/@vellumai/gateway-client/bun.lock +1 -0
- package/node_modules/@vellumai/gateway-client/package.json +3 -1
- package/node_modules/@vellumai/gateway-client/src/__tests__/gateway-client.test.ts +1 -1
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +87 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +3 -5
- package/openapi.yaml +633 -4
- package/package.json +1 -3
- package/src/__tests__/adaptive-thinking-repair.test.ts +185 -0
- package/src/__tests__/agent-loop-compaction-events.test.ts +7 -6
- package/src/__tests__/anthropic-provider.test.ts +129 -0
- package/src/__tests__/background-workers-disk-pressure.test.ts +4 -1
- package/src/__tests__/btw-routes.test.ts +7 -34
- package/src/__tests__/checker.test.ts +6 -12
- package/src/__tests__/config-loader-backfill.test.ts +4 -2
- package/src/__tests__/config-loader-quarantine-notice.test.ts +167 -0
- package/src/__tests__/config-watcher.test.ts +2 -2
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -1
- package/src/__tests__/conversation-error.test.ts +2 -6
- package/src/__tests__/conversation-history-web-search.test.ts +8 -0
- package/src/__tests__/conversation-title-service.test.ts +2 -1
- package/src/__tests__/credential-security-invariants.test.ts +1 -1
- package/src/__tests__/disk-pressure-tools.test.ts +1 -1
- package/src/__tests__/exploration-drift-hook.test.ts +692 -0
- package/src/__tests__/filing-service.test.ts +8 -3
- package/src/__tests__/guardian-action-store.test.ts +0 -167
- package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +1 -1
- package/src/__tests__/heartbeat-disk-pressure.test.ts +4 -1
- package/src/__tests__/heartbeat-service.test.ts +5 -2
- package/src/__tests__/identity-intro-cache.test.ts +12 -5
- package/src/__tests__/identity-routes.test.ts +16 -57
- package/src/__tests__/injector-chain.test.ts +8 -3
- package/src/__tests__/injector-config-quarantine-notice.test.ts +115 -0
- package/src/__tests__/llm-catalog-parity.test.ts +16 -0
- package/src/__tests__/llm-usage-store.test.ts +11 -0
- package/src/__tests__/log-export-workspace.test.ts +468 -3
- package/src/__tests__/memory-v2-static-injector.test.ts +22 -0
- package/src/__tests__/model-intents.test.ts +1 -1
- package/src/__tests__/oauth-cli.test.ts +19 -8
- package/src/__tests__/openai-provider.test.ts +34 -0
- package/src/__tests__/prechat-onboarding-contract.test.ts +0 -1
- package/src/__tests__/recurrence-engine.test.ts +45 -0
- package/src/__tests__/schedule-routes.test.ts +34 -0
- package/src/__tests__/scheduler-disk-pressure.test.ts +1 -1
- package/src/__tests__/script-proxy-conversation-manager.test.ts +10 -5
- package/src/__tests__/secret-fixtures.ts +20 -0
- package/src/__tests__/skill-tool-factory.test.ts +49 -0
- package/src/__tests__/subagent-role-registry.test.ts +24 -1
- package/src/__tests__/subagent-tools.test.ts +1 -0
- package/src/__tests__/system-prompt.test.ts +109 -11
- package/src/__tests__/tool-approval-handler.test.ts +85 -0
- package/src/__tests__/tool-audit-listener.test.ts +86 -0
- package/src/__tests__/tool-error-hook.test.ts +1 -0
- package/src/__tests__/tool-result-spool.test.ts +337 -0
- package/src/__tests__/tool-result-truncate-hook.test.ts +1 -0
- package/src/__tests__/validate-input.test.ts +95 -1
- package/src/__tests__/workspace-migration-098-remove-stale-updates-bulletin-file.test.ts +65 -0
- package/src/__tests__/workspace-migration-099-disable-cache-one-shot-callsites.test.ts +139 -0
- package/src/__tests__/workspace-migration-100-upgrade-quality-profile-to-fable-5.test.ts +174 -0
- package/src/__tests__/workspace-migration-101-upgrade-balanced-economy-to-minimax-m3.test.ts +162 -0
- package/src/__tests__/workspace-release-notes-feature-flag-guard.test.ts +45 -95
- package/src/acp/__tests__/agent-process.test.ts +315 -2
- package/src/acp/__tests__/prepare-agent-env.test.ts +79 -5
- package/src/acp/agent-process.ts +163 -34
- package/src/acp/prepare-agent-env.ts +55 -15
- package/src/agent/loop.ts +81 -24
- package/src/api/events/usage-progress.ts +28 -0
- package/src/api/index.ts +6 -0
- package/src/background-wake/wake-intent-hooks.test.ts +2 -0
- package/src/bundler/app-bundler.ts +25 -42
- package/src/bundler/app-compiler.ts +8 -0
- package/src/calls/call-controller.ts +1 -1
- package/src/cli/commands/plugins.ts +248 -15
- package/src/cli/lib/__tests__/inspect-plugin.test.ts +318 -0
- package/src/cli/lib/__tests__/install-from-github.test.ts +16 -9
- package/src/cli/lib/__tests__/plugin-artifact.test.ts +183 -0
- package/src/cli/lib/__tests__/plugin-details.test.ts +158 -0
- package/src/cli/lib/__tests__/plugin-fingerprint.test.ts +245 -0
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +307 -0
- package/src/cli/lib/inspect-plugin.ts +252 -0
- package/src/cli/lib/install-from-github.ts +214 -21
- package/src/cli/lib/list-installed-plugins.ts +17 -6
- package/src/cli/lib/plugin-artifact.ts +103 -0
- package/src/cli/lib/plugin-details.ts +18 -1
- package/src/cli/lib/plugin-fingerprint.ts +197 -0
- package/src/cli/lib/upgrade-plugin.ts +225 -0
- package/src/config/bundled-skills/subagent/SKILL.md +2 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +8 -2
- package/src/config/call-site-defaults.ts +13 -2
- package/src/config/feature-flag-registry.json +8 -16
- package/src/config/loader.ts +52 -59
- package/src/config/schema.ts +0 -2
- package/src/config/schemas/__tests__/memory-v2.test.ts +1 -0
- package/src/config/schemas/__tests__/memory-v3.test.ts +10 -0
- package/src/config/schemas/llm.ts +10 -0
- package/src/config/schemas/memory-v2.ts +13 -0
- package/src/config/schemas/memory-v3.ts +92 -0
- package/src/config/seed-inference-profiles.ts +4 -8
- package/src/context/post-turn-tool-result-truncation.ts +32 -18
- package/src/context/tool-result-spool.ts +104 -0
- package/src/credential-execution/feature-gates.ts +0 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +41 -16
- package/src/daemon/conversation-error.ts +6 -15
- package/src/daemon/conversation.ts +9 -0
- package/src/daemon/disk-pressure-policy.ts +0 -1
- package/src/daemon/lifecycle.ts +1 -20
- package/src/daemon/message-types/conversations.ts +2 -15
- package/src/daemon/trust-context.ts +1 -1
- package/src/events/tool-audit-listener.ts +40 -9
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +1 -1
- package/src/home/__tests__/home-content-refresh.test.ts +114 -0
- package/src/home/__tests__/suggested-prompts.test.ts +86 -5
- package/src/home/home-content-refresh.ts +43 -31
- package/src/home/home-greeting-cache.ts +8 -1
- package/src/home/home-greeting.ts +13 -9
- package/src/home/suggested-prompts.ts +77 -24
- package/src/ipc/routes/trust-rules.test.ts +66 -72
- package/src/media/image-credentials.ts +2 -2
- package/src/memory/__tests__/compaction-log-store-clickhouse.test.ts +432 -0
- package/src/memory/{compaction-log-writer-clickhouse.ts → compaction-log-store-clickhouse.ts} +264 -55
- package/src/memory/conversation-attention-store.ts +1 -0
- package/src/memory/conversation-bootstrap.ts +18 -9
- package/src/memory/conversation-crud.ts +12 -2
- package/src/memory/conversation-title-service.ts +53 -9
- package/src/memory/delivery-channels.ts +0 -69
- package/src/memory/graph/extraction-job.ts +0 -15
- package/src/memory/guardian-action-store.ts +1 -376
- package/src/memory/llm-usage-store.ts +5 -1
- package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +2 -2
- package/src/memory/v2/__tests__/consolidation-job.test.ts +183 -2
- package/src/memory/v2/__tests__/injection.test.ts +70 -0
- package/src/memory/v2/__tests__/static-context.test.ts +12 -0
- package/src/memory/v2/consolidation-job.ts +93 -9
- package/src/memory/v2/injection.ts +53 -0
- package/src/memory/v2/prompts/consolidation.ts +1 -0
- package/src/memory/v2/static-context.ts +13 -1
- package/src/memory/v2/sweep-job.ts +1 -1
- package/src/memory/v2/types.ts +5 -0
- package/src/plugin-api/types.ts +7 -0
- package/src/plugins/defaults/exploration-drift/hooks/post-tool-use.ts +300 -0
- package/src/plugins/defaults/exploration-drift/package.json +15 -0
- package/src/plugins/defaults/index.ts +25 -0
- package/src/plugins/defaults/memory-retrieval/injectors.ts +132 -4
- package/src/plugins/defaults/memory-v3-shadow/__tests__/card.test.ts +92 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +2 -1
- package/src/plugins/defaults/memory-v3-shadow/__tests__/fresh-set.test.ts +52 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +1 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +2 -1
- package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +136 -5
- package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +17 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +6 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +5 -1
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +68 -4
- package/src/plugins/defaults/memory-v3-shadow/card.ts +49 -5
- package/src/plugins/defaults/memory-v3-shadow/fresh-set.ts +59 -0
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +4 -2
- package/src/plugins/defaults/memory-v3-shadow/learned-edges.test.ts +169 -0
- package/src/plugins/defaults/memory-v3-shadow/learned-edges.ts +178 -0
- package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +115 -26
- package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +13 -9
- package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +144 -22
- package/src/plugins/defaults/memory-v3-shadow/types.ts +24 -6
- package/src/plugins/defaults/title-generate/hooks/stop.ts +13 -0
- package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +16 -0
- package/src/prompts/cache-boundary.ts +17 -0
- package/src/prompts/sections.ts +50 -17
- package/src/prompts/system-prompt.ts +12 -4
- package/src/prompts/templates/system-sections.ts +22 -0
- package/src/providers/__tests__/unparseable-tool-args.test.ts +53 -0
- package/src/providers/anthropic/client.ts +74 -28
- package/src/providers/gemini/client.ts +5 -1
- package/src/providers/minimax/client.ts +9 -0
- package/src/providers/model-catalog.ts +28 -0
- package/src/providers/model-intents.ts +3 -3
- package/src/providers/openai/chat-completions-provider.ts +4 -2
- package/src/providers/openai/responses-provider.ts +7 -2
- package/src/providers/retry.ts +8 -0
- package/src/providers/types.ts +11 -0
- package/src/providers/unparseable-tool-args.ts +56 -0
- package/src/runtime/AGENTS.md +6 -0
- package/src/runtime/__tests__/agent-wake.test.ts +2 -2
- package/src/runtime/agent-wake.ts +5 -5
- package/src/runtime/background-job-runner.ts +2 -2
- package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +150 -3
- package/src/runtime/migrations/vbundle-import-analyzer.ts +29 -6
- package/src/runtime/migrations/vbundle-import-policy.ts +23 -0
- package/src/runtime/migrations/vbundle-importer.ts +9 -4
- package/src/runtime/migrations/vbundle-streaming-importer.ts +8 -3
- package/src/runtime/pre-first-message-gate.ts +1 -1
- package/src/runtime/routes/__tests__/conversation-compaction-routes.test.ts +241 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +132 -0
- package/src/runtime/routes/__tests__/gateway-log-routes.test.ts +97 -185
- package/src/runtime/routes/__tests__/home-feed-routes.test.ts +17 -0
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +348 -0
- package/src/runtime/routes/__tests__/task-routes.test.ts +3 -3
- package/src/runtime/routes/btw-routes.ts +0 -14
- package/src/runtime/routes/conversation-compaction-routes.ts +86 -19
- package/src/runtime/routes/conversation-list-routes.ts +77 -5
- package/src/runtime/routes/conversation-management-routes.ts +54 -0
- package/src/runtime/routes/conversation-query-routes.ts +79 -4
- package/src/runtime/routes/gateway-log-routes.ts +14 -64
- package/src/runtime/routes/home-feed-routes.ts +10 -0
- package/src/runtime/routes/identity-intro-cache.ts +1 -1
- package/src/runtime/routes/identity-routes.ts +76 -20
- package/src/runtime/routes/inbound-message-handler.ts +0 -36
- package/src/runtime/routes/log-export-routes.ts +143 -96
- package/src/runtime/routes/plugins-routes.ts +380 -0
- package/src/runtime/routes/redact-staged-export.ts +259 -0
- package/src/runtime/routes/schedule-routes.ts +19 -2
- package/src/runtime/routes/trust-rules-routes.ts +14 -67
- package/src/schedule/recurrence-engine.ts +34 -0
- package/src/schedule/scheduler.ts +1 -0
- package/src/security/redact-json.ts +61 -0
- package/src/skills/validate-input.ts +41 -1
- package/src/subagent/types.ts +26 -1
- package/src/telemetry/types.ts +15 -1
- package/src/telemetry/usage-telemetry-reporter.test.ts +6 -1
- package/src/telemetry/usage-telemetry-reporter.ts +1 -0
- package/src/tools/apps/executors.ts +1 -1
- package/src/tools/skills/skill-tool-factory.ts +19 -8
- package/src/tools/tool-approval-handler.ts +31 -0
- package/src/usage/types.ts +8 -1
- package/src/util/platform.ts +16 -0
- package/src/watcher/engine.ts +1 -0
- package/src/workspace/adaptive-thinking-repair.ts +113 -0
- package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +70 -67
- package/src/workspace/migrations/098-remove-stale-updates-bulletin-file.ts +31 -0
- package/src/workspace/migrations/099-disable-cache-one-shot-callsites.ts +81 -0
- package/src/workspace/migrations/100-upgrade-quality-profile-to-fable-5.ts +86 -0
- package/src/workspace/migrations/101-upgrade-balanced-economy-to-minimax-m3.ts +70 -0
- package/src/workspace/migrations/registry.ts +8 -0
- package/src/__tests__/config-loader-quarantine-bulletin.test.ts +0 -202
- package/src/__tests__/conversation-starters-cadence.test.ts +0 -161
- package/src/__tests__/guardian-action-followup-executor.test.ts +0 -322
- package/src/__tests__/guardian-action-followup-store.test.ts +0 -373
- package/src/__tests__/guardian-action-late-reply.test.ts +0 -1083
- package/src/__tests__/update-bulletin-job.test.ts +0 -292
- package/src/config/schemas/updates.ts +0 -14
- package/src/memory/__tests__/compaction-log-writer-clickhouse.test.ts +0 -227
- package/src/memory/conversation-starters-cadence.ts +0 -78
- package/src/prompts/update-bulletin-job.ts +0 -180
- package/src/runtime/guardian-action-followup-executor.ts +0 -306
package/src/memory/{compaction-log-writer-clickhouse.ts → compaction-log-store-clickhouse.ts}
RENAMED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ClickHouse compaction log
|
|
2
|
+
* ClickHouse compaction log store.
|
|
3
3
|
*
|
|
4
4
|
* Consumes the agent loop's compaction start/end event pair
|
|
5
5
|
* (`context_compacting` / `compaction_completed`, correlated by
|
|
@@ -10,8 +10,12 @@
|
|
|
10
10
|
*
|
|
11
11
|
* The table is event-sourced rather than upserted: a `start` row records
|
|
12
12
|
* that an attempt began (so attempts that never complete — pipeline throw,
|
|
13
|
-
* turn abort — are still visible), and an `end` row carries the
|
|
14
|
-
* `ContextWindowResult
|
|
13
|
+
* turn abort — are still visible), and an `end` row carries the
|
|
14
|
+
* `ContextWindowResult` fields unnested on the event. Consumers pair rows
|
|
15
|
+
* by `compaction_id`.
|
|
16
|
+
*
|
|
17
|
+
* The store also serves reads: the compaction-trail route pairs the rows
|
|
18
|
+
* back into per-attempt events via `getEventsBetween`.
|
|
15
19
|
*
|
|
16
20
|
* Writes are strictly best-effort: every entry point swallows and logs
|
|
17
21
|
* failures so a ClickHouse outage can never abort a turn. URL and password
|
|
@@ -27,7 +31,7 @@ import { credentialKey } from "../security/credential-key.js";
|
|
|
27
31
|
import { getSecureKeyAsync } from "../security/secure-keys.js";
|
|
28
32
|
import { getLogger } from "../util/logger.js";
|
|
29
33
|
|
|
30
|
-
const log = getLogger("compaction-log-
|
|
34
|
+
const log = getLogger("compaction-log-store");
|
|
31
35
|
|
|
32
36
|
export type CompactionStartEvent = Extract<
|
|
33
37
|
AgentEvent,
|
|
@@ -58,7 +62,7 @@ interface CompactionLogRow {
|
|
|
58
62
|
finished_at: number;
|
|
59
63
|
duration_ms: number;
|
|
60
64
|
pre_message_count: number;
|
|
61
|
-
|
|
65
|
+
result_message_count: number;
|
|
62
66
|
compacted: number;
|
|
63
67
|
previous_estimated_input_tokens: number;
|
|
64
68
|
estimated_input_tokens: number;
|
|
@@ -87,10 +91,47 @@ async function readCredentialOrNull(
|
|
|
87
91
|
return value ?? null;
|
|
88
92
|
}
|
|
89
93
|
|
|
94
|
+
/**
|
|
95
|
+
* One compaction attempt, reconstructed from its start/end row pair.
|
|
96
|
+
* `null` means the value isn't known — either the field belongs to the
|
|
97
|
+
* other phase's row, the attempt never completed, or the stored value was
|
|
98
|
+
* the column's sentinel.
|
|
99
|
+
*/
|
|
100
|
+
export interface CompactionLogEvent {
|
|
101
|
+
compactionId: string;
|
|
102
|
+
requestId: string;
|
|
103
|
+
trigger: string;
|
|
104
|
+
startedAt: number;
|
|
105
|
+
finishedAt: number | null;
|
|
106
|
+
durationMs: number | null;
|
|
107
|
+
preMessageCount: number | null;
|
|
108
|
+
resultMessageCount: number | null;
|
|
109
|
+
compacted: boolean | null;
|
|
110
|
+
previousEstimatedInputTokens: number | null;
|
|
111
|
+
estimatedInputTokens: number | null;
|
|
112
|
+
maxInputTokens: number | null;
|
|
113
|
+
thresholdTokens: number | null;
|
|
114
|
+
compactedMessages: number | null;
|
|
115
|
+
compactedPersistedMessages: number | null;
|
|
116
|
+
preservedTailMessages: number | null;
|
|
117
|
+
summaryCalls: number | null;
|
|
118
|
+
summaryInputTokens: number | null;
|
|
119
|
+
summaryOutputTokens: number | null;
|
|
120
|
+
summaryModel: string | null;
|
|
121
|
+
summaryFailed: boolean | null;
|
|
122
|
+
reason: string | null;
|
|
123
|
+
exhausted: boolean | null;
|
|
124
|
+
injectionMode: string | null;
|
|
125
|
+
autoCompressApplied: boolean | null;
|
|
126
|
+
summaryText: string | null;
|
|
127
|
+
/** True when the end row exists — the attempt ran to completion. */
|
|
128
|
+
completed: boolean;
|
|
129
|
+
}
|
|
130
|
+
|
|
90
131
|
/** Injectable fetch override for tests. Defaults to globalThis.fetch. */
|
|
91
132
|
export type CompactionLogFetch = typeof fetch;
|
|
92
133
|
|
|
93
|
-
export interface
|
|
134
|
+
export interface ClickHouseCompactionLogStoreDeps {
|
|
94
135
|
/** Override the credential read for `clickhouse:url`. */
|
|
95
136
|
resolveUrl?: () => Promise<string | null>;
|
|
96
137
|
/** Override the credential read for `clickhouse:password`. */
|
|
@@ -101,7 +142,7 @@ export interface ClickHouseCompactionLogWriterDeps {
|
|
|
101
142
|
fetchImpl?: CompactionLogFetch;
|
|
102
143
|
}
|
|
103
144
|
|
|
104
|
-
export class
|
|
145
|
+
export class ClickHouseCompactionLogStore {
|
|
105
146
|
private cachedUrl: string | null = null;
|
|
106
147
|
private cachedPassword: string | null = null;
|
|
107
148
|
private cachedAssistantId: string | null = null;
|
|
@@ -114,7 +155,7 @@ export class ClickHouseCompactionLogWriter {
|
|
|
114
155
|
|
|
115
156
|
constructor(
|
|
116
157
|
private readonly config: CompactionLogsClickHouseConfig,
|
|
117
|
-
deps:
|
|
158
|
+
deps: ClickHouseCompactionLogStoreDeps = {},
|
|
118
159
|
) {
|
|
119
160
|
this.resolveUrl =
|
|
120
161
|
deps.resolveUrl ?? (() => readCredentialOrNull("clickhouse", "url"));
|
|
@@ -142,35 +183,105 @@ export class ClickHouseCompactionLogWriter {
|
|
|
142
183
|
conversationId: string,
|
|
143
184
|
event: CompactionEndEvent,
|
|
144
185
|
): Promise<void> {
|
|
145
|
-
const r = event.result;
|
|
146
186
|
await this.insert({
|
|
147
187
|
...emptyRow(await this.assistantId(), conversationId, event),
|
|
148
188
|
phase: "end",
|
|
149
189
|
finished_at: event.finishedAt,
|
|
150
190
|
duration_ms: event.finishedAt - event.startedAt,
|
|
151
|
-
|
|
152
|
-
compacted:
|
|
153
|
-
previous_estimated_input_tokens:
|
|
154
|
-
estimated_input_tokens:
|
|
155
|
-
max_input_tokens:
|
|
156
|
-
threshold_tokens:
|
|
157
|
-
compacted_messages:
|
|
158
|
-
compacted_persisted_messages:
|
|
159
|
-
preserved_tail_messages:
|
|
160
|
-
summary_calls:
|
|
161
|
-
summary_input_tokens:
|
|
162
|
-
summary_output_tokens:
|
|
163
|
-
summary_model:
|
|
191
|
+
result_message_count: event.messages.length,
|
|
192
|
+
compacted: event.compacted ? 1 : 0,
|
|
193
|
+
previous_estimated_input_tokens: event.previousEstimatedInputTokens,
|
|
194
|
+
estimated_input_tokens: event.estimatedInputTokens,
|
|
195
|
+
max_input_tokens: event.maxInputTokens,
|
|
196
|
+
threshold_tokens: event.thresholdTokens,
|
|
197
|
+
compacted_messages: event.compactedMessages,
|
|
198
|
+
compacted_persisted_messages: event.compactedPersistedMessages,
|
|
199
|
+
preserved_tail_messages: event.preservedTailMessages ?? -1,
|
|
200
|
+
summary_calls: event.summaryCalls,
|
|
201
|
+
summary_input_tokens: event.summaryInputTokens,
|
|
202
|
+
summary_output_tokens: event.summaryOutputTokens,
|
|
203
|
+
summary_model: event.summaryModel,
|
|
164
204
|
summary_failed:
|
|
165
|
-
|
|
166
|
-
reason:
|
|
167
|
-
exhausted:
|
|
168
|
-
injection_mode:
|
|
169
|
-
auto_compress_applied:
|
|
170
|
-
summary_text:
|
|
205
|
+
event.summaryFailed === undefined ? -1 : event.summaryFailed ? 1 : 0,
|
|
206
|
+
reason: event.reason ?? "",
|
|
207
|
+
exhausted: event.exhausted ? 1 : 0,
|
|
208
|
+
injection_mode: event.injectionMode ?? "",
|
|
209
|
+
auto_compress_applied: event.autoCompressApplied ? 1 : 0,
|
|
210
|
+
summary_text: event.summaryText.slice(0, SUMMARY_TEXT_MAX_CHARS),
|
|
171
211
|
});
|
|
172
212
|
}
|
|
173
213
|
|
|
214
|
+
/**
|
|
215
|
+
* Read all compaction attempts whose `started_at` falls strictly inside
|
|
216
|
+
* the given window, pairing start/end rows by `compaction_id`. Mirrors
|
|
217
|
+
* the strict `>` / `<` predicate contract of
|
|
218
|
+
* `getCompactionLogsBetween` on the llm-request-log sources, so the
|
|
219
|
+
* route can apply the same ±1ms boundary shifts to both.
|
|
220
|
+
*/
|
|
221
|
+
async getEventsBetween(
|
|
222
|
+
conversationId: string,
|
|
223
|
+
afterStartedAt: number | null,
|
|
224
|
+
beforeStartedAt: number,
|
|
225
|
+
): Promise<CompactionLogEvent[]> {
|
|
226
|
+
const params: Record<string, string> = {
|
|
227
|
+
assistant_id: await this.assistantId(),
|
|
228
|
+
conversation_id: conversationId,
|
|
229
|
+
before_started_at: String(beforeStartedAt),
|
|
230
|
+
};
|
|
231
|
+
// Type-bound parameter slots referenced in the SQL but unbound at exec
|
|
232
|
+
// time return a server error, so the floor predicate is only templated
|
|
233
|
+
// in when the caller has one.
|
|
234
|
+
let afterPredicate = "";
|
|
235
|
+
if (afterStartedAt !== null) {
|
|
236
|
+
params.after_started_at = String(afterStartedAt);
|
|
237
|
+
afterPredicate =
|
|
238
|
+
" AND started_at > fromUnixTimestamp64Milli({after_started_at:Int64})";
|
|
239
|
+
}
|
|
240
|
+
const sql = `SELECT
|
|
241
|
+
compaction_id,
|
|
242
|
+
request_id,
|
|
243
|
+
phase,
|
|
244
|
+
trigger,
|
|
245
|
+
toUnixTimestamp64Milli(started_at) AS started_at,
|
|
246
|
+
toUnixTimestamp64Milli(finished_at) AS finished_at,
|
|
247
|
+
duration_ms,
|
|
248
|
+
pre_message_count,
|
|
249
|
+
result_message_count,
|
|
250
|
+
compacted,
|
|
251
|
+
previous_estimated_input_tokens,
|
|
252
|
+
estimated_input_tokens,
|
|
253
|
+
max_input_tokens,
|
|
254
|
+
threshold_tokens,
|
|
255
|
+
compacted_messages,
|
|
256
|
+
compacted_persisted_messages,
|
|
257
|
+
preserved_tail_messages,
|
|
258
|
+
summary_calls,
|
|
259
|
+
summary_input_tokens,
|
|
260
|
+
summary_output_tokens,
|
|
261
|
+
summary_model,
|
|
262
|
+
summary_failed,
|
|
263
|
+
reason,
|
|
264
|
+
exhausted,
|
|
265
|
+
injection_mode,
|
|
266
|
+
auto_compress_applied,
|
|
267
|
+
summary_text
|
|
268
|
+
FROM ${this.tableRef()}
|
|
269
|
+
WHERE assistant_id = {assistant_id:String}
|
|
270
|
+
AND conversation_id = {conversation_id:String}
|
|
271
|
+
AND started_at < fromUnixTimestamp64Milli({before_started_at:Int64})${afterPredicate}
|
|
272
|
+
ORDER BY started_at ASC, created_at ASC
|
|
273
|
+
LIMIT 1 BY compaction_id, phase
|
|
274
|
+
FORMAT JSONEachRow`;
|
|
275
|
+
const text = await this.exec(sql, undefined, params);
|
|
276
|
+
const rows: Record<string, unknown>[] = [];
|
|
277
|
+
for (const line of text.split("\n")) {
|
|
278
|
+
const trimmed = line.trim();
|
|
279
|
+
if (trimmed.length === 0) continue;
|
|
280
|
+
rows.push(JSON.parse(trimmed) as Record<string, unknown>);
|
|
281
|
+
}
|
|
282
|
+
return pairRowsIntoEvents(rows);
|
|
283
|
+
}
|
|
284
|
+
|
|
174
285
|
private async insert(row: CompactionLogRow): Promise<void> {
|
|
175
286
|
await this.ensureTable();
|
|
176
287
|
await this.exec(
|
|
@@ -198,7 +309,7 @@ export class ClickHouseCompactionLogWriter {
|
|
|
198
309
|
finished_at DateTime64(3),
|
|
199
310
|
duration_ms Int64,
|
|
200
311
|
pre_message_count Int64,
|
|
201
|
-
|
|
312
|
+
result_message_count Int64,
|
|
202
313
|
compacted UInt8,
|
|
203
314
|
previous_estimated_input_tokens Int64,
|
|
204
315
|
estimated_input_tokens Int64,
|
|
@@ -220,10 +331,13 @@ export class ClickHouseCompactionLogWriter {
|
|
|
220
331
|
created_at DateTime64(3) DEFAULT now64(3)
|
|
221
332
|
) ENGINE = MergeTree
|
|
222
333
|
ORDER BY (assistant_id, conversation_id, started_at, compaction_id)`,
|
|
223
|
-
).
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
334
|
+
).then(
|
|
335
|
+
() => undefined,
|
|
336
|
+
(err: unknown) => {
|
|
337
|
+
this.ensureTablePromise = null;
|
|
338
|
+
throw err;
|
|
339
|
+
},
|
|
340
|
+
);
|
|
227
341
|
}
|
|
228
342
|
return this.ensureTablePromise;
|
|
229
343
|
}
|
|
@@ -235,7 +349,11 @@ export class ClickHouseCompactionLogWriter {
|
|
|
235
349
|
return `\`${this.config.table.replace(/`/g, "``")}\``;
|
|
236
350
|
}
|
|
237
351
|
|
|
238
|
-
private async exec(
|
|
352
|
+
private async exec(
|
|
353
|
+
sql: string,
|
|
354
|
+
body?: string,
|
|
355
|
+
params?: Record<string, string>,
|
|
356
|
+
): Promise<string> {
|
|
239
357
|
const baseUrl = await this.url();
|
|
240
358
|
const password = await this.password();
|
|
241
359
|
|
|
@@ -248,6 +366,9 @@ export class ClickHouseCompactionLogWriter {
|
|
|
248
366
|
);
|
|
249
367
|
}
|
|
250
368
|
target.searchParams.set("database", this.config.database);
|
|
369
|
+
for (const [k, v] of Object.entries(params ?? {})) {
|
|
370
|
+
target.searchParams.set(`param_${k}`, v);
|
|
371
|
+
}
|
|
251
372
|
// For INSERTs the statement goes in the `query` param and the row data
|
|
252
373
|
// in the body; DDL ships as the body itself.
|
|
253
374
|
// Integer timestamps land correctly in DateTime64(3) columns: ClickHouse
|
|
@@ -270,11 +391,10 @@ export class ClickHouseCompactionLogWriter {
|
|
|
270
391
|
if (!res.ok) {
|
|
271
392
|
const text = await res.text().catch(() => "");
|
|
272
393
|
throw new Error(
|
|
273
|
-
`ClickHouse compaction-log
|
|
394
|
+
`ClickHouse compaction-log request failed (HTTP ${res.status}): ${text.slice(0, 500)}`,
|
|
274
395
|
);
|
|
275
396
|
}
|
|
276
|
-
|
|
277
|
-
await res.text().catch(() => "");
|
|
397
|
+
return res.text();
|
|
278
398
|
}
|
|
279
399
|
|
|
280
400
|
private async assistantId(): Promise<string> {
|
|
@@ -331,7 +451,7 @@ function emptyRow(
|
|
|
331
451
|
finished_at: 0,
|
|
332
452
|
duration_ms: -1,
|
|
333
453
|
pre_message_count: -1,
|
|
334
|
-
|
|
454
|
+
result_message_count: -1,
|
|
335
455
|
compacted: 0,
|
|
336
456
|
previous_estimated_input_tokens: -1,
|
|
337
457
|
estimated_input_tokens: -1,
|
|
@@ -353,32 +473,121 @@ function emptyRow(
|
|
|
353
473
|
};
|
|
354
474
|
}
|
|
355
475
|
|
|
476
|
+
function num(value: unknown): number {
|
|
477
|
+
return typeof value === "number" ? value : Number(value);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/** Map the `-1` "unknown count" column sentinel back to null. */
|
|
481
|
+
function countOrNull(value: unknown): number | null {
|
|
482
|
+
const n = num(value);
|
|
483
|
+
return Number.isFinite(n) && n >= 0 ? n : null;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/** Map the `''` "not set" string column sentinel back to null. */
|
|
487
|
+
function strOrNull(value: unknown): string | null {
|
|
488
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
489
|
+
}
|
|
490
|
+
|
|
356
491
|
/**
|
|
357
|
-
*
|
|
492
|
+
* Pair raw start/end rows by `compaction_id` into per-attempt events.
|
|
493
|
+
* Start-only pairs (the attempt never completed) surface with end-phase
|
|
494
|
+
* fields null and `completed: false`.
|
|
495
|
+
*/
|
|
496
|
+
function pairRowsIntoEvents(
|
|
497
|
+
rows: Record<string, unknown>[],
|
|
498
|
+
): CompactionLogEvent[] {
|
|
499
|
+
const byId = new Map<
|
|
500
|
+
string,
|
|
501
|
+
{ start?: Record<string, unknown>; end?: Record<string, unknown> }
|
|
502
|
+
>();
|
|
503
|
+
for (const row of rows) {
|
|
504
|
+
const id = String(row.compaction_id);
|
|
505
|
+
const pair = byId.get(id) ?? {};
|
|
506
|
+
if (row.phase === "end") pair.end = row;
|
|
507
|
+
else pair.start = row;
|
|
508
|
+
byId.set(id, pair);
|
|
509
|
+
}
|
|
510
|
+
const events: CompactionLogEvent[] = [];
|
|
511
|
+
for (const [compactionId, { start, end }] of byId) {
|
|
512
|
+
const base = end ?? start;
|
|
513
|
+
if (!base) continue;
|
|
514
|
+
events.push({
|
|
515
|
+
compactionId,
|
|
516
|
+
requestId: String(base.request_id ?? ""),
|
|
517
|
+
trigger: String(base.trigger ?? ""),
|
|
518
|
+
startedAt: num(base.started_at),
|
|
519
|
+
finishedAt: end ? num(end.finished_at) : null,
|
|
520
|
+
durationMs: end ? countOrNull(end.duration_ms) : null,
|
|
521
|
+
preMessageCount: start ? countOrNull(start.pre_message_count) : null,
|
|
522
|
+
resultMessageCount: end ? countOrNull(end.result_message_count) : null,
|
|
523
|
+
compacted: end ? num(end.compacted) === 1 : null,
|
|
524
|
+
previousEstimatedInputTokens: end
|
|
525
|
+
? countOrNull(end.previous_estimated_input_tokens)
|
|
526
|
+
: null,
|
|
527
|
+
estimatedInputTokens: end
|
|
528
|
+
? countOrNull(end.estimated_input_tokens)
|
|
529
|
+
: null,
|
|
530
|
+
maxInputTokens: end ? countOrNull(end.max_input_tokens) : null,
|
|
531
|
+
thresholdTokens: end ? countOrNull(end.threshold_tokens) : null,
|
|
532
|
+
compactedMessages: end ? countOrNull(end.compacted_messages) : null,
|
|
533
|
+
compactedPersistedMessages: end
|
|
534
|
+
? countOrNull(end.compacted_persisted_messages)
|
|
535
|
+
: null,
|
|
536
|
+
preservedTailMessages: end
|
|
537
|
+
? countOrNull(end.preserved_tail_messages)
|
|
538
|
+
: null,
|
|
539
|
+
summaryCalls: end ? countOrNull(end.summary_calls) : null,
|
|
540
|
+
summaryInputTokens: end ? countOrNull(end.summary_input_tokens) : null,
|
|
541
|
+
summaryOutputTokens: end ? countOrNull(end.summary_output_tokens) : null,
|
|
542
|
+
summaryModel: end ? strOrNull(end.summary_model) : null,
|
|
543
|
+
summaryFailed:
|
|
544
|
+
end && num(end.summary_failed) !== -1
|
|
545
|
+
? num(end.summary_failed) === 1
|
|
546
|
+
: null,
|
|
547
|
+
reason: end ? strOrNull(end.reason) : null,
|
|
548
|
+
exhausted: end ? num(end.exhausted) === 1 : null,
|
|
549
|
+
injectionMode: end ? strOrNull(end.injection_mode) : null,
|
|
550
|
+
autoCompressApplied: end ? num(end.auto_compress_applied) === 1 : null,
|
|
551
|
+
summaryText: end ? strOrNull(end.summary_text) : null,
|
|
552
|
+
completed: end !== undefined,
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
return events.sort(
|
|
556
|
+
(a, b) =>
|
|
557
|
+
a.startedAt - b.startedAt || a.compactionId.localeCompare(b.compactionId),
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Store cache keyed by the serialized connection config so config edits
|
|
358
563
|
* take effect on the next event without restarting, while steady-state
|
|
359
564
|
* writes reuse one instance (and its cached credentials / ensured table).
|
|
360
565
|
*/
|
|
361
|
-
let
|
|
566
|
+
let cachedStore: {
|
|
362
567
|
key: string;
|
|
363
|
-
|
|
568
|
+
store: ClickHouseCompactionLogStore;
|
|
364
569
|
} | null = null;
|
|
365
570
|
|
|
366
|
-
|
|
571
|
+
/**
|
|
572
|
+
* Resolve the configured ClickHouse compaction log store, or null when
|
|
573
|
+
* `compactionLogs.destination` is not `"clickhouse"`.
|
|
574
|
+
*/
|
|
575
|
+
export function getCompactionLogStore(): ClickHouseCompactionLogStore | null {
|
|
367
576
|
const cfg = getConfig().compactionLogs;
|
|
368
577
|
if (!cfg || cfg.destination !== "clickhouse") return null;
|
|
369
578
|
const key = JSON.stringify(cfg.clickhouse);
|
|
370
|
-
if (!
|
|
371
|
-
|
|
579
|
+
if (!cachedStore || cachedStore.key !== key) {
|
|
580
|
+
cachedStore = {
|
|
372
581
|
key,
|
|
373
|
-
|
|
582
|
+
store: new ClickHouseCompactionLogStore(cfg.clickhouse),
|
|
374
583
|
};
|
|
375
584
|
}
|
|
376
|
-
return
|
|
585
|
+
return cachedStore.store;
|
|
377
586
|
}
|
|
378
587
|
|
|
379
|
-
/** Test hook: drop the memoized
|
|
380
|
-
export function
|
|
381
|
-
|
|
588
|
+
/** Test hook: drop the memoized store so config/dep changes are picked up. */
|
|
589
|
+
export function resetCompactionLogStoreForTests(): void {
|
|
590
|
+
cachedStore = null;
|
|
382
591
|
}
|
|
383
592
|
|
|
384
593
|
/**
|
|
@@ -389,9 +598,9 @@ export function recordCompactionStartBestEffort(
|
|
|
389
598
|
conversationId: string,
|
|
390
599
|
event: CompactionStartEvent,
|
|
391
600
|
): void {
|
|
392
|
-
const
|
|
393
|
-
if (!
|
|
394
|
-
|
|
601
|
+
const store = getCompactionLogStore();
|
|
602
|
+
if (!store) return;
|
|
603
|
+
store.writeStart(conversationId, event).catch((err: unknown) => {
|
|
395
604
|
log.warn(
|
|
396
605
|
{ err, conversationId, compactionId: event.compactionId },
|
|
397
606
|
"Failed to write compaction start log (non-fatal)",
|
|
@@ -407,9 +616,9 @@ export function recordCompactionEndBestEffort(
|
|
|
407
616
|
conversationId: string,
|
|
408
617
|
event: CompactionEndEvent,
|
|
409
618
|
): void {
|
|
410
|
-
const
|
|
411
|
-
if (!
|
|
412
|
-
|
|
619
|
+
const store = getCompactionLogStore();
|
|
620
|
+
if (!store) return;
|
|
621
|
+
store.writeEnd(conversationId, event).catch((err: unknown) => {
|
|
413
622
|
log.warn(
|
|
414
623
|
{ err, conversationId, compactionId: event.compactionId },
|
|
415
624
|
"Failed to write compaction end log (non-fatal)",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createConversation } from "./conversation-crud.js";
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
AUTO_TITLE_DETERMINISTIC,
|
|
4
|
+
deriveDeterministicTitle,
|
|
5
5
|
type TitleOrigin,
|
|
6
6
|
} from "./conversation-title-service.js";
|
|
7
7
|
|
|
@@ -22,9 +22,23 @@ export interface BootstrapConversationOptions {
|
|
|
22
22
|
forkParentConversationId?: string;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Create a system-initiated conversation with a deterministic title derived
|
|
27
|
+
* from `systemHint` — no LLM call. Background conversations (heartbeat runs,
|
|
28
|
+
* scheduled jobs, subagents, retrospectives) are created in bulk and rarely
|
|
29
|
+
* read by name, so an LLM-generated title is not worth the tokens. The title
|
|
30
|
+
* is persisted with `AUTO_TITLE_DETERMINISTIC`, which keeps it replaceable:
|
|
31
|
+
* if a user ever sends a message in the conversation, the title-generate
|
|
32
|
+
* hook upgrades it to an LLM title (see
|
|
33
|
+
* `plugins/defaults/title-generate/hooks/user-prompt-submit.ts`).
|
|
34
|
+
*/
|
|
25
35
|
export function bootstrapConversation(opts: BootstrapConversationOptions) {
|
|
26
|
-
|
|
27
|
-
title:
|
|
36
|
+
return createConversation({
|
|
37
|
+
title: deriveDeterministicTitle({
|
|
38
|
+
origin: opts.origin,
|
|
39
|
+
systemHint: opts.systemHint,
|
|
40
|
+
}),
|
|
41
|
+
isAutoTitle: AUTO_TITLE_DETERMINISTIC,
|
|
28
42
|
...(opts.conversationType && { conversationType: opts.conversationType }),
|
|
29
43
|
...(opts.source && { source: opts.source }),
|
|
30
44
|
...(opts.scheduleJobId && { scheduleJobId: opts.scheduleJobId }),
|
|
@@ -33,9 +47,4 @@ export function bootstrapConversation(opts: BootstrapConversationOptions) {
|
|
|
33
47
|
forkParentConversationId: opts.forkParentConversationId,
|
|
34
48
|
}),
|
|
35
49
|
});
|
|
36
|
-
queueGenerateConversationTitle({
|
|
37
|
-
conversationId: conversation.id,
|
|
38
|
-
context: { origin: opts.origin, systemHint: opts.systemHint },
|
|
39
|
-
});
|
|
40
|
-
return conversation;
|
|
41
50
|
}
|
|
@@ -537,6 +537,13 @@ export function createConversation(
|
|
|
537
537
|
| string
|
|
538
538
|
| {
|
|
539
539
|
title?: string;
|
|
540
|
+
/**
|
|
541
|
+
* Override the `is_auto_title` column (schema default 1). Pass
|
|
542
|
+
* `AUTO_TITLE_DETERMINISTIC` (2) when the title was derived
|
|
543
|
+
* deterministically at bootstrap so later generation passes know
|
|
544
|
+
* they may replace it.
|
|
545
|
+
*/
|
|
546
|
+
isAutoTitle?: number;
|
|
540
547
|
conversationType?: ConversationCreateType;
|
|
541
548
|
source?: string;
|
|
542
549
|
scheduleJobId?: string;
|
|
@@ -566,6 +573,9 @@ export function createConversation(
|
|
|
566
573
|
const conversation = {
|
|
567
574
|
id,
|
|
568
575
|
title: opts.title ?? null,
|
|
576
|
+
...(opts.isAutoTitle !== undefined
|
|
577
|
+
? { isAutoTitle: opts.isAutoTitle }
|
|
578
|
+
: {}),
|
|
569
579
|
createdAt: now,
|
|
570
580
|
updatedAt: now,
|
|
571
581
|
totalInputTokens: 0,
|
|
@@ -2039,8 +2049,8 @@ export interface OverrideProfileFields {
|
|
|
2039
2049
|
/**
|
|
2040
2050
|
* Resolve the per-turn inference-profile override from a conversation's
|
|
2041
2051
|
* fields. Returns the `inferenceProfile` for interactive conversations,
|
|
2042
|
-
* `undefined` for automation threads (subagent fan-out, scheduled
|
|
2043
|
-
*
|
|
2052
|
+
* `undefined` for automation threads (subagent fan-out, scheduled
|
|
2053
|
+
* tasks) so they run on the workspace defaults rather than
|
|
2044
2054
|
* inheriting an interactive override.
|
|
2045
2055
|
*/
|
|
2046
2056
|
export function resolveOverrideProfile(
|
|
@@ -39,7 +39,6 @@ export type TitleOrigin =
|
|
|
39
39
|
| "filing"
|
|
40
40
|
| "local"
|
|
41
41
|
| "task_submit"
|
|
42
|
-
| "updates_bulletin"
|
|
43
42
|
| "memory_consolidation"
|
|
44
43
|
| "memory_retrospective"
|
|
45
44
|
| "misc";
|
|
@@ -63,6 +62,21 @@ export interface TitleContext {
|
|
|
63
62
|
export const GENERATING_TITLE = "Generating title...";
|
|
64
63
|
const UNTITLED_FALLBACK = "Untitled Conversation";
|
|
65
64
|
|
|
65
|
+
// ── `conversations.isAutoTitle` values ───────────────────────────────
|
|
66
|
+
//
|
|
67
|
+
// Most readers treat the column as a boolean ("not user-set"); the two
|
|
68
|
+
// distinct non-zero values let the title pipeline tell HOW a title was set.
|
|
69
|
+
|
|
70
|
+
/** Title was generated by an LLM from conversation content. */
|
|
71
|
+
export const AUTO_TITLE_LLM = 1;
|
|
72
|
+
/**
|
|
73
|
+
* Title was derived deterministically from bootstrap context — no LLM call,
|
|
74
|
+
* no user input. Unlike LLM titles, these stay replaceable: the first
|
|
75
|
+
* genuine user message in the conversation upgrades them to an LLM title
|
|
76
|
+
* (see `canReplaceTitle`).
|
|
77
|
+
*/
|
|
78
|
+
export const AUTO_TITLE_DETERMINISTIC = 2;
|
|
79
|
+
|
|
66
80
|
// ── Replaceability check ─────────────────────────────────────────────
|
|
67
81
|
|
|
68
82
|
const REPLACEABLE_PATTERNS = [
|
|
@@ -83,6 +97,35 @@ export function isReplaceableTitle(title: string | null): boolean {
|
|
|
83
97
|
return REPLACEABLE_PATTERNS.some((pattern) => pattern.test(title));
|
|
84
98
|
}
|
|
85
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Whether auto-generation may overwrite the conversation's current title.
|
|
102
|
+
* True for placeholder/system titles and for deterministic bootstrap titles
|
|
103
|
+
* (`AUTO_TITLE_DETERMINISTIC`); false for user-set custom titles and titles
|
|
104
|
+
* already generated by an LLM.
|
|
105
|
+
*/
|
|
106
|
+
function canReplaceTitle(conversation: {
|
|
107
|
+
title: string | null;
|
|
108
|
+
isAutoTitle: number;
|
|
109
|
+
}): boolean {
|
|
110
|
+
return (
|
|
111
|
+
isReplaceableTitle(conversation.title) ||
|
|
112
|
+
conversation.isAutoTitle === AUTO_TITLE_DETERMINISTIC
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Derive a deterministic title from bootstrap context without an LLM call.
|
|
118
|
+
* Used for background/system conversations (heartbeat runs, scheduled jobs,
|
|
119
|
+
* subagents, retrospectives) where an LLM-generated title is not worth the
|
|
120
|
+
* tokens — the bootstrap `systemHint` already names the work. Persist it
|
|
121
|
+
* with `AUTO_TITLE_DETERMINISTIC` so a later genuine user message can still
|
|
122
|
+
* upgrade it to an LLM title.
|
|
123
|
+
*/
|
|
124
|
+
export function deriveDeterministicTitle(context: TitleContext): string {
|
|
125
|
+
const base = deriveFallbackTitle(context) ?? UNTITLED_FALLBACK;
|
|
126
|
+
return truncateTitle(base.replace(/\s+/g, " ").trim());
|
|
127
|
+
}
|
|
128
|
+
|
|
86
129
|
// ── Title generation ─────────────────────────────────────────────────
|
|
87
130
|
|
|
88
131
|
export interface GenerateTitleParams {
|
|
@@ -111,16 +154,17 @@ export async function generateAndPersistConversationTitle(
|
|
|
111
154
|
|
|
112
155
|
// Check current title is replaceable
|
|
113
156
|
const conversation = getConversation(conversationId);
|
|
114
|
-
if (conversation && !
|
|
157
|
+
if (conversation && !canReplaceTitle(conversation)) {
|
|
115
158
|
return { title: conversation.title!, updated: false };
|
|
116
159
|
}
|
|
117
160
|
|
|
118
161
|
const provider =
|
|
119
162
|
params.provider ?? (await getConfiguredProvider("conversationTitle"));
|
|
120
163
|
if (!provider) {
|
|
121
|
-
// No provider available — fall back to context-derived title or untitled
|
|
164
|
+
// No provider available — fall back to context-derived title or untitled.
|
|
165
|
+
// Deterministic, so keep it upgradeable by a later generation pass.
|
|
122
166
|
const fallback = deriveFallbackTitle(context) ?? UNTITLED_FALLBACK;
|
|
123
|
-
updateConversationTitle(conversationId, fallback,
|
|
167
|
+
updateConversationTitle(conversationId, fallback, AUTO_TITLE_DETERMINISTIC);
|
|
124
168
|
publishConversationTitleChanged(conversationId, fallback);
|
|
125
169
|
return { title: fallback, updated: true };
|
|
126
170
|
}
|
|
@@ -139,11 +183,11 @@ export async function generateAndPersistConversationTitle(
|
|
|
139
183
|
if (title) {
|
|
140
184
|
// Re-check replaceability before persisting (race guard)
|
|
141
185
|
const current = getConversation(conversationId);
|
|
142
|
-
if (current && !
|
|
186
|
+
if (current && !canReplaceTitle(current)) {
|
|
143
187
|
return { title: current.title!, updated: false };
|
|
144
188
|
}
|
|
145
189
|
|
|
146
|
-
updateConversationTitle(conversationId, title,
|
|
190
|
+
updateConversationTitle(conversationId, title, AUTO_TITLE_LLM);
|
|
147
191
|
publishConversationTitleChanged(conversationId, title);
|
|
148
192
|
log.info({ conversationId, title }, "Auto-generated conversation title");
|
|
149
193
|
return { title, updated: true };
|
|
@@ -155,12 +199,12 @@ export async function generateAndPersistConversationTitle(
|
|
|
155
199
|
// while the LLM request was in-flight; writing unconditionally would
|
|
156
200
|
// clobber the user's intent.
|
|
157
201
|
const currentForFallback = getConversation(conversationId);
|
|
158
|
-
if (currentForFallback && !
|
|
202
|
+
if (currentForFallback && !canReplaceTitle(currentForFallback)) {
|
|
159
203
|
return { title: currentForFallback.title!, updated: false };
|
|
160
204
|
}
|
|
161
205
|
|
|
162
206
|
const fallback = deriveFallbackTitle(context) ?? UNTITLED_FALLBACK;
|
|
163
|
-
updateConversationTitle(conversationId, fallback,
|
|
207
|
+
updateConversationTitle(conversationId, fallback, AUTO_TITLE_DETERMINISTIC);
|
|
164
208
|
publishConversationTitleChanged(conversationId, fallback);
|
|
165
209
|
return { title: fallback, updated: true };
|
|
166
210
|
}
|
|
@@ -277,7 +321,7 @@ export async function regenerateConversationTitle(
|
|
|
277
321
|
return { title: current?.title ?? UNTITLED_FALLBACK, updated: false };
|
|
278
322
|
}
|
|
279
323
|
|
|
280
|
-
updateConversationTitle(conversationId, title,
|
|
324
|
+
updateConversationTitle(conversationId, title, AUTO_TITLE_LLM);
|
|
281
325
|
publishConversationTitleChanged(conversationId, title);
|
|
282
326
|
log.info(
|
|
283
327
|
{ conversationId, title },
|