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
15 changes: 15 additions & 0 deletions services/session-ingest/src/ingest/default-session-title.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Duplicated from kilocode packages/opencode/src/session/session.ts:55-62
// (isDefaultTitle). Keep in sync when the CLI default-title pattern changes.
export const DEFAULT_SESSION_TITLE_PATTERN =
/^(New session - |Child session - )\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;

/**
* A session title counts as the creation placeholder when it is still NULL or when it
* holds the default title stamped at creation time (e.g. cloud-agent-next inserts
* `New session - <ISO timestamp>` via createSessionForCloudAgent). Agent-generated
* titles may promote a placeholder title but must never overwrite a user-chosen one.
*/
export function isDefaultSessionTitle(title: string | null | undefined): boolean {
if (title == null) return true;
return DEFAULT_SESSION_TITLE_PATTERN.test(title);
}
18 changes: 18 additions & 0 deletions services/session-ingest/src/ingest/metadata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,24 @@ describe('applyMetadataChanges', () => {
);
});

it('applies the agent-generated title over the default title stamped at creation (cloud-agent-next)', async () => {
// cloud-agent-next creates rows with a non-null default title
// (`New session - <ISO timestamp>`) via createSessionForCloudAgent; that title is
// still the creation placeholder and must not block the agent-generated title.
const db = createApplyMetadataDb({ initialTitle: 'New session - 2026-08-04T10:00:00.000Z' });
vi.mocked(getWorkerDb).mockReturnValue(db as never);

await applyMetadataChanges(env, 'usr_1', 'ses_1', new Map([['title', 'Agent title']]));

expect(db.updateSets).toEqual([expect.objectContaining({ title: 'Agent title' })]);
expect(notifyUserSessionEvent).toHaveBeenCalledWith(
env,
'usr_1',
expect.objectContaining({ type: 'session.updated' }),
undefined
);
});

it('skips the agent-generated title write when the user already renamed the session', async () => {
const db = createApplyMetadataDb({ initialTitle: 'User chosen title' });
vi.mocked(getWorkerDb).mockReturnValue(db as never);
Expand Down
12 changes: 7 additions & 5 deletions services/session-ingest/src/ingest/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { getSessionAccessCacheDO } from '../dos/SessionAccessCacheDO';
import { isNeedsInputStatus } from '../dos/session-ingest-attention';
import { mapSessionEventRow, notifyUserSessionEvent } from '../session-events';
import { SessionStatusSchema } from '../types/user-connection-protocol';
import { isDefaultSessionTitle } from './default-session-title';

/** Stored status written when a CLI disconnects while the session is waiting on input. */
export const CLI_DISCONNECT_ATTENTION_RESET_STATUS = 'retry' as const;
Expand Down Expand Up @@ -131,12 +132,13 @@ export async function applyMetadataChanges(
})();

// Agent-generated titles arrive asynchronously and can race a user rename. A session's
// title starts out NULL (the placeholder set at row creation); only promote it from that
// placeholder here. Once the title is non-null — whether from a user rename or an earlier
// agent-generated write — leave it alone so a later user rename can never be clobbered by
// an in-flight ingest.
// title starts out as a creation placeholder — NULL, or the default title stamped at
// creation (e.g. cloud-agent-next inserts `New session - <ISO timestamp>`); only promote
// it from that placeholder here. Once the title holds anything else — whether from a user
// rename or an earlier agent-generated write — leave it alone so a later user rename can
// never be clobbered by an in-flight ingest.
if (mergedChanges.has('title')) {
if (currentRow.title === null) {
if (isDefaultSessionTitle(currentRow.title)) {
titleWriteApplied = true;
} else {
console.warn('Skipping agent-generated title write; title is no longer the placeholder', {
Expand Down
11 changes: 1 addition & 10 deletions services/session-ingest/src/routes/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { getUserConnectionDO } from '../dos/UserConnectionDO';
import { getSessionExport } from '../services/session-export';
import { mapSessionEventRow, notifyUserSessionEvent } from '../session-events';
import { handleDirectIngestRequest } from '../ingest/direct-ingest';
import { isDefaultSessionTitle } from '../ingest/default-session-title';
import { resolveAccessibleKiloSession } from '../services/session-access';

export type ApiContext = {
Expand Down Expand Up @@ -525,16 +526,6 @@ api.post('/session/:sessionId/unshare', async c => {
return c.json({ success: true }, 200);
});

// Duplicated from kilocode packages/opencode/src/session/session.ts:55-62
// (isDefaultTitle). Keep in sync when the CLI default-title pattern changes.
const DEFAULT_SESSION_TITLE_PATTERN =
/^(New session - |Child session - )\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;

function isDefaultSessionTitle(title: string | null | undefined): boolean {
if (title == null) return true;
return DEFAULT_SESSION_TITLE_PATTERN.test(title);
}

const reportSessionTitleSchema = z.object({
title: z.string().trim().min(1).max(200),
generated: z.boolean(),
Expand Down