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
190 changes: 190 additions & 0 deletions apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2947,6 +2947,196 @@ describe("ProviderRuntimeIngestion", () => {
).toBe("# Plan title");
});

it("titles task activities with the task description, including on completion", async () => {
const harness = await createHarness();
const now = "2026-01-01T00:00:00.000Z";

harness.emit({
type: "task.started",
eventId: asEventId("evt-named-task-started"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-named-task"),
payload: {
taskId: "named-task-1",
description: "Typecheck mobile app",
taskType: "local_bash",
},
});

harness.emit({
type: "task.progress",
eventId: asEventId("evt-named-task-progress"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-named-task"),
payload: {
taskId: "named-task-1",
description: "Typecheck mobile app",
summary: "Running tsc across the mobile workspace.",
},
});

harness.emit({
type: "task.completed",
eventId: asEventId("evt-named-task-completed"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-named-task"),
payload: {
taskId: "named-task-1",
status: "completed",
summary: "Typecheck finished without errors.",
},
});

const thread = await waitForThread(harness.readModel, (entry) =>
entry.activities.some(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-named-task-completed",
),
);

const progress = thread.activities.find(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-named-task-progress",
);
const completed = thread.activities.find(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-named-task-completed",
);

const progressPayload =
progress?.payload && typeof progress.payload === "object"
? (progress.payload as Record<string, unknown>)
: undefined;
const completedPayload =
completed?.payload && typeof completed.payload === "object"
? (completed.payload as Record<string, unknown>)
: undefined;

expect(progress?.summary).toBe("Typecheck mobile app");
expect(progressPayload?.title).toBe("Typecheck mobile app");
expect(completed?.summary).toBe("Task completed");
expect(completedPayload?.title).toBe("Typecheck mobile app");
expect(completedPayload?.summary).toBe("Typecheck finished without errors.");
expect(completedPayload?.detail).toBe("Typecheck finished without errors.");
});

it("titles task completion from task.started when no progress event carried the name", async () => {
const harness = await createHarness();
const now = "2026-01-01T00:00:00.000Z";

harness.emit({
type: "task.started",
eventId: asEventId("evt-fast-task-started"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-fast-task"),
payload: {
taskId: "fast-task-1",
description: "wait for codex review to finish",
taskType: "local_bash",
},
});

harness.emit({
type: "task.completed",
eventId: asEventId("evt-fast-task-completed"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-fast-task"),
payload: {
taskId: "fast-task-1",
status: "completed",
},
});

const thread = await waitForThread(harness.readModel, (entry) =>
entry.activities.some(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-fast-task-completed",
),
);

const completed = thread.activities.find(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-fast-task-completed",
);
const completedPayload =
completed?.payload && typeof completed.payload === "object"
? (completed.payload as Record<string, unknown>)
: undefined;

expect(completedPayload?.title).toBe("wait for codex review to finish");
});

it("titles task completion from persisted activities after the description cache is swept", async () => {
const harness = await createHarness();
const now = "2026-01-01T00:00:00.000Z";

harness.emit({
type: "task.progress",
eventId: asEventId("evt-swept-task-progress"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-swept-task"),
payload: {
taskId: "swept-task-1",
description: "Watch round-3 CI and bots",
summary: "Polling CI checks.",
},
});

await waitForThread(harness.readModel, (entry) =>
entry.activities.some(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-swept-task-progress",
),
);

// session.exited sweeps the in-memory description cache; the completion
// that follows must recover the name from persisted activities.
harness.emit({
type: "session.exited",
eventId: asEventId("evt-swept-task-session-exited"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
payload: {},
});

harness.emit({
type: "task.completed",
eventId: asEventId("evt-swept-task-completed"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-swept-task"),
payload: {
taskId: "swept-task-1",
status: "completed",
summary: "CI is green.",
},
});

const thread = await waitForThread(harness.readModel, (entry) =>
entry.activities.some(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-swept-task-completed",
),
);

const completed = thread.activities.find(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-swept-task-completed",
);
const completedPayload =
completed?.payload && typeof completed.payload === "object"
? (completed.payload as Record<string, unknown>)
: undefined;

expect(completedPayload?.title).toBe("Watch round-3 CI and bots");
});

it("projects structured user input request and resolution as thread activities", async () => {
const harness = await createHarness();
const now = "2026-01-01T00:00:00.000Z";
Expand Down
102 changes: 99 additions & 3 deletions apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,42 @@ import {
import { ServerSettingsService } from "../../serverSettings.ts";

const providerTurnKey = (threadId: ThreadId, turnId: TurnId) => `${threadId}:${turnId}`;
const providerTaskKey = (threadId: ThreadId, taskId: string) => `${threadId}:${taskId}`;

// Fallback when the in-memory description cache no longer has the task name
// (server restart, session-exit sweep, TTL/capacity eviction): earlier
// task.started/task.progress activities for the task are persisted with it.
function findTaskTitleInActivities(
activities: ReadonlyArray<OrchestrationThreadActivity> | undefined,
taskId: string,
): string | undefined {
if (!activities) {
return undefined;
}
for (let index = activities.length - 1; index >= 0; index -= 1) {
const activity = activities[index];
if (!activity || (activity.kind !== "task.started" && activity.kind !== "task.progress")) {
continue;
}
const payload =
activity.payload && typeof activity.payload === "object"
? (activity.payload as { taskId?: unknown; title?: unknown; detail?: unknown })
: undefined;
if (payload?.taskId !== taskId) {
continue;
}
const title =
typeof payload.title === "string"
? payload.title
: activity.kind === "task.started" && typeof payload.detail === "string"
? payload.detail
: undefined;
if (title && title.trim().length > 0) {
return title;
}
}
return undefined;
}

interface AssistantSegmentState {
baseKey: string;
Expand All@@ -53,6 +89,8 @@ const BUFFERED_MESSAGE_TEXT_BY_MESSAGE_ID_CACHE_CAPACITY = 20_000;
const BUFFERED_MESSAGE_TEXT_BY_MESSAGE_ID_TTL = Duration.minutes(120);
const BUFFERED_PROPOSED_PLAN_BY_ID_CACHE_CAPACITY = 10_000;
const BUFFERED_PROPOSED_PLAN_BY_ID_TTL = Duration.minutes(120);
const TASK_DESCRIPTION_BY_TASK_CACHE_CAPACITY = 10_000;
const TASK_DESCRIPTION_BY_TASK_TTL = Duration.minutes(120);
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
const MAX_BUFFERED_ASSISTANT_CHARS = 24_000;
const STRICT_PROVIDER_LIFECYCLE_GUARD = process.env.T3CODE_STRICT_PROVIDER_LIFECYCLE_GUARD !== "0";

Expand DownExpand Up@@ -264,6 +302,7 @@ function requestKindFromCanonicalRequestType(

function runtimeEventToActivities(
event: ProviderRuntimeEvent,
taskTitle?: string,
): ReadonlyArray<OrchestrationThreadActivity> {
const maybeSequence = (() => {
const eventWithSequence = event as ProviderRuntimeEvent & { sessionSequence?: number };
Expand DownExpand Up@@ -473,9 +512,15 @@ function runtimeEventToActivities(
createdAt: event.createdAt,
tone: "info",
kind: "task.progress",
summary: "Reasoning update",
summary:
event.payload.description.trim().length > 0
? truncateDetail(event.payload.description, 120)
: "Reasoning update",
payload: {
taskId: event.payload.taskId,
...(event.payload.description.trim().length > 0
? { title: truncateDetail(event.payload.description, 120) }
: {}),
detail: truncateDetail(event.payload.summary ?? event.payload.description),
...(event.payload.summary ? { summary: truncateDetail(event.payload.summary) } : {}),
...(event.payload.lastToolName ? { lastToolName: event.payload.lastToolName } : {}),
Expand DownExpand Up@@ -503,7 +548,15 @@ function runtimeEventToActivities(
payload: {
taskId: event.payload.taskId,
status: event.payload.status,
...(event.payload.summary ? { detail: truncateDetail(event.payload.summary) } : {}),
...(taskTitle ? { title: truncateDetail(taskTitle, 120) } : {}),
// summary + detail mirror task.progress: clients label the row from
// summary and keep detail for the preview/expanded body.
...(event.payload.summary
? {
summary: truncateDetail(event.payload.summary),
detail: truncateDetail(event.payload.summary),
}
: {}),
...(event.payload.usage !== undefined ? { usage: event.payload.usage } : {}),
},
turnId: toTurnId(event.turnId) ?? null,
Expand DownExpand Up@@ -666,6 +719,27 @@ const make = Effect.gen(function* () {
lookup: () => Effect.succeed({ text: "", createdAt: "" }),
});

// Task names arrive on task.started/task.progress but not on task.completed,
// so remember them per task to title the completion activity.
const taskDescriptionByTaskKey = yield* Cache.make<string, string>({
capacity: TASK_DESCRIPTION_BY_TASK_CACHE_CAPACITY,
timeToLive: TASK_DESCRIPTION_BY_TASK_TTL,
lookup: () => Effect.succeed(""),
});

const rememberTaskDescription = (threadId: ThreadId, taskId: string, description: string) =>
Cache.set(taskDescriptionByTaskKey, providerTaskKey(threadId, taskId), description);

// Entries are left in place after completion so replayed or duplicate
// terminal events stay titled; TTL, capacity, and the session-exit sweep
// bound the cache.
const lookupTaskDescription = (threadId: ThreadId, taskId: string) =>
Cache.getOption(taskDescriptionByTaskKey, providerTaskKey(threadId, taskId)).pipe(
Effect.map((description) =>
Option.filter(description, (value) => value.length > 0).pipe(Option.getOrUndefined),
),
);

const resolveThreadDetail = Effect.fn("resolveThreadDetail")(function* (threadId: ThreadId) {
return yield* projectionSnapshotQuery
.getThreadDetailById(threadId)
Expand DownExpand Up@@ -1090,6 +1164,7 @@ const make = Effect.gen(function* () {
const turnKeys = Array.from(yield* Cache.keys(turnMessageIdsByTurnKey));
const assistantSegmentKeys = Array.from(yield* Cache.keys(assistantSegmentStateByTurnKey));
const proposedPlanKeys = Array.from(yield* Cache.keys(bufferedProposedPlanById));
const taskDescriptionKeys = Array.from(yield* Cache.keys(taskDescriptionByTaskKey));
yield* Effect.forEach(
turnKeys,
(key) =>
Expand DownExpand Up@@ -1125,6 +1200,12 @@ const make = Effect.gen(function* () {
: Effect.void,
{ concurrency: 1 },
).pipe(Effect.asVoid);
yield* Effect.forEach(
taskDescriptionKeys,
(key) =>
key.startsWith(prefix) ? Cache.invalidate(taskDescriptionByTaskKey, key) : Effect.void,
{ concurrency: 1 },
).pipe(Effect.asVoid);
});

const getSourceProposedPlanReferenceForPendingTurnStart = Effect.fn(
Expand DownExpand Up@@ -1654,7 +1735,22 @@ const make = Effect.gen(function* () {
}
}

const activities = runtimeEventToActivities(event);
if (event.type === "task.started" || event.type === "task.progress") {
const description = event.payload.description?.trim();
if (description) {
yield* rememberTaskDescription(thread.id, event.payload.taskId, description);
}
}
let taskTitle: string | undefined;
if (event.type === "task.completed") {
taskTitle = yield* lookupTaskDescription(thread.id, event.payload.taskId);
if (!taskTitle) {
const threadDetail = yield* getLoadedThreadDetail();
taskTitle = findTaskTitleInActivities(threadDetail?.activities, event.payload.taskId);
}
}

const activities = runtimeEventToActivities(event, taskTitle);
yield* Effect.forEach(activities, (activity) =>
providerCommandId(event, "thread-activity-append").pipe(
Effect.flatMap((commandId) =>
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
feat(server): title background-task work-log rows with the task name by t3dotgg · Pull Request #3751 · pingdotgg/t3code · GitHub
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
190 changes: 190 additions & 0 deletions apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2947,6 +2947,196 @@ describe("ProviderRuntimeIngestion", () => {
).toBe("# Plan title");
});

it("titles task activities with the task description, including on completion", async () => {
const harness = await createHarness();
const now = "2026-01-01T00:00:00.000Z";

harness.emit({
type: "task.started",
eventId: asEventId("evt-named-task-started"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-named-task"),
payload: {
taskId: "named-task-1",
description: "Typecheck mobile app",
taskType: "local_bash",
},
});

harness.emit({
type: "task.progress",
eventId: asEventId("evt-named-task-progress"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-named-task"),
payload: {
taskId: "named-task-1",
description: "Typecheck mobile app",
summary: "Running tsc across the mobile workspace.",
},
});

harness.emit({
type: "task.completed",
eventId: asEventId("evt-named-task-completed"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-named-task"),
payload: {
taskId: "named-task-1",
status: "completed",
summary: "Typecheck finished without errors.",
},
});

const thread = await waitForThread(harness.readModel, (entry) =>
entry.activities.some(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-named-task-completed",
),
);

const progress = thread.activities.find(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-named-task-progress",
);
const completed = thread.activities.find(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-named-task-completed",
);

const progressPayload =
progress?.payload && typeof progress.payload === "object"
? (progress.payload as Record<string, unknown>)
: undefined;
const completedPayload =
completed?.payload && typeof completed.payload === "object"
? (completed.payload as Record<string, unknown>)
: undefined;

expect(progress?.summary).toBe("Typecheck mobile app");
expect(progressPayload?.title).toBe("Typecheck mobile app");
expect(completed?.summary).toBe("Task completed");
expect(completedPayload?.title).toBe("Typecheck mobile app");
expect(completedPayload?.summary).toBe("Typecheck finished without errors.");
expect(completedPayload?.detail).toBe("Typecheck finished without errors.");
});

it("titles task completion from task.started when no progress event carried the name", async () => {
const harness = await createHarness();
const now = "2026-01-01T00:00:00.000Z";

harness.emit({
type: "task.started",
eventId: asEventId("evt-fast-task-started"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-fast-task"),
payload: {
taskId: "fast-task-1",
description: "wait for codex review to finish",
taskType: "local_bash",
},
});

harness.emit({
type: "task.completed",
eventId: asEventId("evt-fast-task-completed"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-fast-task"),
payload: {
taskId: "fast-task-1",
status: "completed",
},
});

const thread = await waitForThread(harness.readModel, (entry) =>
entry.activities.some(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-fast-task-completed",
),
);

const completed = thread.activities.find(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-fast-task-completed",
);
const completedPayload =
completed?.payload && typeof completed.payload === "object"
? (completed.payload as Record<string, unknown>)
: undefined;

expect(completedPayload?.title).toBe("wait for codex review to finish");
});

it("titles task completion from persisted activities after the description cache is swept", async () => {
const harness = await createHarness();
const now = "2026-01-01T00:00:00.000Z";

harness.emit({
type: "task.progress",
eventId: asEventId("evt-swept-task-progress"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-swept-task"),
payload: {
taskId: "swept-task-1",
description: "Watch round-3 CI and bots",
summary: "Polling CI checks.",
},
});

await waitForThread(harness.readModel, (entry) =>
entry.activities.some(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-swept-task-progress",
),
);

// session.exited sweeps the in-memory description cache; the completion
// that follows must recover the name from persisted activities.
harness.emit({
type: "session.exited",
eventId: asEventId("evt-swept-task-session-exited"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
payload: {},
});

harness.emit({
type: "task.completed",
eventId: asEventId("evt-swept-task-completed"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-swept-task"),
payload: {
taskId: "swept-task-1",
status: "completed",
summary: "CI is green.",
},
});

const thread = await waitForThread(harness.readModel, (entry) =>
entry.activities.some(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-swept-task-completed",
),
);

const completed = thread.activities.find(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-swept-task-completed",
);
const completedPayload =
completed?.payload && typeof completed.payload === "object"
? (completed.payload as Record<string, unknown>)
: undefined;

expect(completedPayload?.title).toBe("Watch round-3 CI and bots");
});

it("projects structured user input request and resolution as thread activities", async () => {
const harness = await createHarness();
const now = "2026-01-01T00:00:00.000Z";
Expand Down
102 changes: 99 additions & 3 deletions apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,42 @@ import {
import { ServerSettingsService } from "../../serverSettings.ts";

const providerTurnKey = (threadId: ThreadId, turnId: TurnId) => `${threadId}:${turnId}`;
const providerTaskKey = (threadId: ThreadId, taskId: string) => `${threadId}:${taskId}`;

// Fallback when the in-memory description cache no longer has the task name
// (server restart, session-exit sweep, TTL/capacity eviction): earlier
// task.started/task.progress activities for the task are persisted with it.
function findTaskTitleInActivities(
activities: ReadonlyArray<OrchestrationThreadActivity> | undefined,
taskId: string,
): string | undefined {
if (!activities) {
return undefined;
}
for (let index = activities.length - 1; index >= 0; index -= 1) {
const activity = activities[index];
if (!activity || (activity.kind !== "task.started" && activity.kind !== "task.progress")) {
continue;
}
const payload =
activity.payload && typeof activity.payload === "object"
? (activity.payload as { taskId?: unknown; title?: unknown; detail?: unknown })
: undefined;
if (payload?.taskId !== taskId) {
continue;
}
const title =
typeof payload.title === "string"
? payload.title
: activity.kind === "task.started" && typeof payload.detail === "string"
? payload.detail
: undefined;
if (title && title.trim().length > 0) {
return title;
}
}
return undefined;
}

interface AssistantSegmentState {
baseKey: string;
Expand All@@ -53,6 +89,8 @@ const BUFFERED_MESSAGE_TEXT_BY_MESSAGE_ID_CACHE_CAPACITY = 20_000;
const BUFFERED_MESSAGE_TEXT_BY_MESSAGE_ID_TTL = Duration.minutes(120);
const BUFFERED_PROPOSED_PLAN_BY_ID_CACHE_CAPACITY = 10_000;
const BUFFERED_PROPOSED_PLAN_BY_ID_TTL = Duration.minutes(120);
const TASK_DESCRIPTION_BY_TASK_CACHE_CAPACITY = 10_000;
const TASK_DESCRIPTION_BY_TASK_TTL = Duration.minutes(120);
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
const MAX_BUFFERED_ASSISTANT_CHARS = 24_000;
const STRICT_PROVIDER_LIFECYCLE_GUARD = process.env.T3CODE_STRICT_PROVIDER_LIFECYCLE_GUARD !== "0";

Expand DownExpand Up@@ -264,6 +302,7 @@ function requestKindFromCanonicalRequestType(

function runtimeEventToActivities(
event: ProviderRuntimeEvent,
taskTitle?: string,
): ReadonlyArray<OrchestrationThreadActivity> {
const maybeSequence = (() => {
const eventWithSequence = event as ProviderRuntimeEvent & { sessionSequence?: number };
Expand DownExpand Up@@ -473,9 +512,15 @@ function runtimeEventToActivities(
createdAt: event.createdAt,
tone: "info",
kind: "task.progress",
summary: "Reasoning update",
summary:
event.payload.description.trim().length > 0
? truncateDetail(event.payload.description, 120)
: "Reasoning update",
payload: {
taskId: event.payload.taskId,
...(event.payload.description.trim().length > 0
? { title: truncateDetail(event.payload.description, 120) }
: {}),
detail: truncateDetail(event.payload.summary ?? event.payload.description),
...(event.payload.summary ? { summary: truncateDetail(event.payload.summary) } : {}),
...(event.payload.lastToolName ? { lastToolName: event.payload.lastToolName } : {}),
Expand DownExpand Up@@ -503,7 +548,15 @@ function runtimeEventToActivities(
payload: {
taskId: event.payload.taskId,
status: event.payload.status,
...(event.payload.summary ? { detail: truncateDetail(event.payload.summary) } : {}),
...(taskTitle ? { title: truncateDetail(taskTitle, 120) } : {}),
// summary + detail mirror task.progress: clients label the row from
// summary and keep detail for the preview/expanded body.
...(event.payload.summary
? {
summary: truncateDetail(event.payload.summary),
detail: truncateDetail(event.payload.summary),
}
: {}),
...(event.payload.usage !== undefined ? { usage: event.payload.usage } : {}),
},
turnId: toTurnId(event.turnId) ?? null,
Expand DownExpand Up@@ -666,6 +719,27 @@ const make = Effect.gen(function* () {
lookup: () => Effect.succeed({ text: "", createdAt: "" }),
});

// Task names arrive on task.started/task.progress but not on task.completed,
// so remember them per task to title the completion activity.
const taskDescriptionByTaskKey = yield* Cache.make<string, string>({
capacity: TASK_DESCRIPTION_BY_TASK_CACHE_CAPACITY,
timeToLive: TASK_DESCRIPTION_BY_TASK_TTL,
lookup: () => Effect.succeed(""),
});

const rememberTaskDescription = (threadId: ThreadId, taskId: string, description: string) =>
Cache.set(taskDescriptionByTaskKey, providerTaskKey(threadId, taskId), description);

// Entries are left in place after completion so replayed or duplicate
// terminal events stay titled; TTL, capacity, and the session-exit sweep
// bound the cache.
const lookupTaskDescription = (threadId: ThreadId, taskId: string) =>
Cache.getOption(taskDescriptionByTaskKey, providerTaskKey(threadId, taskId)).pipe(
Effect.map((description) =>
Option.filter(description, (value) => value.length > 0).pipe(Option.getOrUndefined),
),
);

const resolveThreadDetail = Effect.fn("resolveThreadDetail")(function* (threadId: ThreadId) {
return yield* projectionSnapshotQuery
.getThreadDetailById(threadId)
Expand DownExpand Up@@ -1090,6 +1164,7 @@ const make = Effect.gen(function* () {
const turnKeys = Array.from(yield* Cache.keys(turnMessageIdsByTurnKey));
const assistantSegmentKeys = Array.from(yield* Cache.keys(assistantSegmentStateByTurnKey));
const proposedPlanKeys = Array.from(yield* Cache.keys(bufferedProposedPlanById));
const taskDescriptionKeys = Array.from(yield* Cache.keys(taskDescriptionByTaskKey));
yield* Effect.forEach(
turnKeys,
(key) =>
Expand DownExpand Up@@ -1125,6 +1200,12 @@ const make = Effect.gen(function* () {
: Effect.void,
{ concurrency: 1 },
).pipe(Effect.asVoid);
yield* Effect.forEach(
taskDescriptionKeys,
(key) =>
key.startsWith(prefix) ? Cache.invalidate(taskDescriptionByTaskKey, key) : Effect.void,
{ concurrency: 1 },
).pipe(Effect.asVoid);
});

const getSourceProposedPlanReferenceForPendingTurnStart = Effect.fn(
Expand DownExpand Up@@ -1654,7 +1735,22 @@ const make = Effect.gen(function* () {
}
}

const activities = runtimeEventToActivities(event);
if (event.type === "task.started" || event.type === "task.progress") {
const description = event.payload.description?.trim();
if (description) {
yield* rememberTaskDescription(thread.id, event.payload.taskId, description);
}
}
let taskTitle: string | undefined;
if (event.type === "task.completed") {
taskTitle = yield* lookupTaskDescription(thread.id, event.payload.taskId);
if (!taskTitle) {
const threadDetail = yield* getLoadedThreadDetail();
taskTitle = findTaskTitleInActivities(threadDetail?.activities, event.payload.taskId);
}
}

const activities = runtimeEventToActivities(event, taskTitle);
yield* Effect.forEach(activities, (activity) =>
providerCommandId(event, "thread-activity-append").pipe(
Effect.flatMap((commandId) =>
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(server): title background-task work-log rows with the task name by t3dotgg · Pull Request #3751 · pingdotgg/t3code · GitHub
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
190 changes: 190 additions & 0 deletions apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2947,6 +2947,196 @@ describe("ProviderRuntimeIngestion", () => {
).toBe("# Plan title");
});

it("titles task activities with the task description, including on completion", async () => {
const harness = await createHarness();
const now = "2026-01-01T00:00:00.000Z";

harness.emit({
type: "task.started",
eventId: asEventId("evt-named-task-started"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-named-task"),
payload: {
taskId: "named-task-1",
description: "Typecheck mobile app",
taskType: "local_bash",
},
});

harness.emit({
type: "task.progress",
eventId: asEventId("evt-named-task-progress"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-named-task"),
payload: {
taskId: "named-task-1",
description: "Typecheck mobile app",
summary: "Running tsc across the mobile workspace.",
},
});

harness.emit({
type: "task.completed",
eventId: asEventId("evt-named-task-completed"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-named-task"),
payload: {
taskId: "named-task-1",
status: "completed",
summary: "Typecheck finished without errors.",
},
});

const thread = await waitForThread(harness.readModel, (entry) =>
entry.activities.some(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-named-task-completed",
),
);

const progress = thread.activities.find(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-named-task-progress",
);
const completed = thread.activities.find(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-named-task-completed",
);

const progressPayload =
progress?.payload && typeof progress.payload === "object"
? (progress.payload as Record<string, unknown>)
: undefined;
const completedPayload =
completed?.payload && typeof completed.payload === "object"
? (completed.payload as Record<string, unknown>)
: undefined;

expect(progress?.summary).toBe("Typecheck mobile app");
expect(progressPayload?.title).toBe("Typecheck mobile app");
expect(completed?.summary).toBe("Task completed");
expect(completedPayload?.title).toBe("Typecheck mobile app");
expect(completedPayload?.summary).toBe("Typecheck finished without errors.");
expect(completedPayload?.detail).toBe("Typecheck finished without errors.");
});

it("titles task completion from task.started when no progress event carried the name", async () => {
const harness = await createHarness();
const now = "2026-01-01T00:00:00.000Z";

harness.emit({
type: "task.started",
eventId: asEventId("evt-fast-task-started"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-fast-task"),
payload: {
taskId: "fast-task-1",
description: "wait for codex review to finish",
taskType: "local_bash",
},
});

harness.emit({
type: "task.completed",
eventId: asEventId("evt-fast-task-completed"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-fast-task"),
payload: {
taskId: "fast-task-1",
status: "completed",
},
});

const thread = await waitForThread(harness.readModel, (entry) =>
entry.activities.some(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-fast-task-completed",
),
);

const completed = thread.activities.find(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-fast-task-completed",
);
const completedPayload =
completed?.payload && typeof completed.payload === "object"
? (completed.payload as Record<string, unknown>)
: undefined;

expect(completedPayload?.title).toBe("wait for codex review to finish");
});

it("titles task completion from persisted activities after the description cache is swept", async () => {
const harness = await createHarness();
const now = "2026-01-01T00:00:00.000Z";

harness.emit({
type: "task.progress",
eventId: asEventId("evt-swept-task-progress"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-swept-task"),
payload: {
taskId: "swept-task-1",
description: "Watch round-3 CI and bots",
summary: "Polling CI checks.",
},
});

await waitForThread(harness.readModel, (entry) =>
entry.activities.some(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-swept-task-progress",
),
);

// session.exited sweeps the in-memory description cache; the completion
// that follows must recover the name from persisted activities.
harness.emit({
type: "session.exited",
eventId: asEventId("evt-swept-task-session-exited"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
payload: {},
});

harness.emit({
type: "task.completed",
eventId: asEventId("evt-swept-task-completed"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-swept-task"),
payload: {
taskId: "swept-task-1",
status: "completed",
summary: "CI is green.",
},
});

const thread = await waitForThread(harness.readModel, (entry) =>
entry.activities.some(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-swept-task-completed",
),
);

const completed = thread.activities.find(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-swept-task-completed",
);
const completedPayload =
completed?.payload && typeof completed.payload === "object"
? (completed.payload as Record<string, unknown>)
: undefined;

expect(completedPayload?.title).toBe("Watch round-3 CI and bots");
});

it("projects structured user input request and resolution as thread activities", async () => {
const harness = await createHarness();
const now = "2026-01-01T00:00:00.000Z";
Expand Down
102 changes: 99 additions & 3 deletions apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,42 @@ import {
import { ServerSettingsService } from "../../serverSettings.ts";

const providerTurnKey = (threadId: ThreadId, turnId: TurnId) => `${threadId}:${turnId}`;
const providerTaskKey = (threadId: ThreadId, taskId: string) => `${threadId}:${taskId}`;

// Fallback when the in-memory description cache no longer has the task name
// (server restart, session-exit sweep, TTL/capacity eviction): earlier
// task.started/task.progress activities for the task are persisted with it.
function findTaskTitleInActivities(
activities: ReadonlyArray<OrchestrationThreadActivity> | undefined,
taskId: string,
): string | undefined {
if (!activities) {
return undefined;
}
for (let index = activities.length - 1; index >= 0; index -= 1) {
const activity = activities[index];
if (!activity || (activity.kind !== "task.started" && activity.kind !== "task.progress")) {
continue;
}
const payload =
activity.payload && typeof activity.payload === "object"
? (activity.payload as { taskId?: unknown; title?: unknown; detail?: unknown })
: undefined;
if (payload?.taskId !== taskId) {
continue;
}
const title =
typeof payload.title === "string"
? payload.title
: activity.kind === "task.started" && typeof payload.detail === "string"
? payload.detail
: undefined;
if (title && title.trim().length > 0) {
return title;
}
}
return undefined;
}

interface AssistantSegmentState {
baseKey: string;
Expand All@@ -53,6 +89,8 @@ const BUFFERED_MESSAGE_TEXT_BY_MESSAGE_ID_CACHE_CAPACITY = 20_000;
const BUFFERED_MESSAGE_TEXT_BY_MESSAGE_ID_TTL = Duration.minutes(120);
const BUFFERED_PROPOSED_PLAN_BY_ID_CACHE_CAPACITY = 10_000;
const BUFFERED_PROPOSED_PLAN_BY_ID_TTL = Duration.minutes(120);
const TASK_DESCRIPTION_BY_TASK_CACHE_CAPACITY = 10_000;
const TASK_DESCRIPTION_BY_TASK_TTL = Duration.minutes(120);
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
const MAX_BUFFERED_ASSISTANT_CHARS = 24_000;
const STRICT_PROVIDER_LIFECYCLE_GUARD = process.env.T3CODE_STRICT_PROVIDER_LIFECYCLE_GUARD !== "0";

Expand DownExpand Up@@ -264,6 +302,7 @@ function requestKindFromCanonicalRequestType(

function runtimeEventToActivities(
event: ProviderRuntimeEvent,
taskTitle?: string,
): ReadonlyArray<OrchestrationThreadActivity> {
const maybeSequence = (() => {
const eventWithSequence = event as ProviderRuntimeEvent & { sessionSequence?: number };
Expand DownExpand Up@@ -473,9 +512,15 @@ function runtimeEventToActivities(
createdAt: event.createdAt,
tone: "info",
kind: "task.progress",
summary: "Reasoning update",
summary:
event.payload.description.trim().length > 0
? truncateDetail(event.payload.description, 120)
: "Reasoning update",
payload: {
taskId: event.payload.taskId,
...(event.payload.description.trim().length > 0
? { title: truncateDetail(event.payload.description, 120) }
: {}),
detail: truncateDetail(event.payload.summary ?? event.payload.description),
...(event.payload.summary ? { summary: truncateDetail(event.payload.summary) } : {}),
...(event.payload.lastToolName ? { lastToolName: event.payload.lastToolName } : {}),
Expand DownExpand Up@@ -503,7 +548,15 @@ function runtimeEventToActivities(
payload: {
taskId: event.payload.taskId,
status: event.payload.status,
...(event.payload.summary ? { detail: truncateDetail(event.payload.summary) } : {}),
...(taskTitle ? { title: truncateDetail(taskTitle, 120) } : {}),
// summary + detail mirror task.progress: clients label the row from
// summary and keep detail for the preview/expanded body.
...(event.payload.summary
? {
summary: truncateDetail(event.payload.summary),
detail: truncateDetail(event.payload.summary),
}
: {}),
...(event.payload.usage !== undefined ? { usage: event.payload.usage } : {}),
},
turnId: toTurnId(event.turnId) ?? null,
Expand DownExpand Up@@ -666,6 +719,27 @@ const make = Effect.gen(function* () {
lookup: () => Effect.succeed({ text: "", createdAt: "" }),
});

// Task names arrive on task.started/task.progress but not on task.completed,
// so remember them per task to title the completion activity.
const taskDescriptionByTaskKey = yield* Cache.make<string, string>({
capacity: TASK_DESCRIPTION_BY_TASK_CACHE_CAPACITY,
timeToLive: TASK_DESCRIPTION_BY_TASK_TTL,
lookup: () => Effect.succeed(""),
});

const rememberTaskDescription = (threadId: ThreadId, taskId: string, description: string) =>
Cache.set(taskDescriptionByTaskKey, providerTaskKey(threadId, taskId), description);

// Entries are left in place after completion so replayed or duplicate
// terminal events stay titled; TTL, capacity, and the session-exit sweep
// bound the cache.
const lookupTaskDescription = (threadId: ThreadId, taskId: string) =>
Cache.getOption(taskDescriptionByTaskKey, providerTaskKey(threadId, taskId)).pipe(
Effect.map((description) =>
Option.filter(description, (value) => value.length > 0).pipe(Option.getOrUndefined),
),
);

const resolveThreadDetail = Effect.fn("resolveThreadDetail")(function* (threadId: ThreadId) {
return yield* projectionSnapshotQuery
.getThreadDetailById(threadId)
Expand DownExpand Up@@ -1090,6 +1164,7 @@ const make = Effect.gen(function* () {
const turnKeys = Array.from(yield* Cache.keys(turnMessageIdsByTurnKey));
const assistantSegmentKeys = Array.from(yield* Cache.keys(assistantSegmentStateByTurnKey));
const proposedPlanKeys = Array.from(yield* Cache.keys(bufferedProposedPlanById));
const taskDescriptionKeys = Array.from(yield* Cache.keys(taskDescriptionByTaskKey));
yield* Effect.forEach(
turnKeys,
(key) =>
Expand DownExpand Up@@ -1125,6 +1200,12 @@ const make = Effect.gen(function* () {
: Effect.void,
{ concurrency: 1 },
).pipe(Effect.asVoid);
yield* Effect.forEach(
taskDescriptionKeys,
(key) =>
key.startsWith(prefix) ? Cache.invalidate(taskDescriptionByTaskKey, key) : Effect.void,
{ concurrency: 1 },
).pipe(Effect.asVoid);
});

const getSourceProposedPlanReferenceForPendingTurnStart = Effect.fn(
Expand DownExpand Up@@ -1654,7 +1735,22 @@ const make = Effect.gen(function* () {
}
}

const activities = runtimeEventToActivities(event);
if (event.type === "task.started" || event.type === "task.progress") {
const description = event.payload.description?.trim();
if (description) {
yield* rememberTaskDescription(thread.id, event.payload.taskId, description);
}
}
let taskTitle: string | undefined;
if (event.type === "task.completed") {
taskTitle = yield* lookupTaskDescription(thread.id, event.payload.taskId);
if (!taskTitle) {
const threadDetail = yield* getLoadedThreadDetail();
taskTitle = findTaskTitleInActivities(threadDetail?.activities, event.payload.taskId);
}
}

const activities = runtimeEventToActivities(event, taskTitle);
yield* Effect.forEach(activities, (activity) =>
providerCommandId(event, "thread-activity-append").pipe(
Effect.flatMap((commandId) =>
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(server): title background-task work-log rows with the task name by t3dotgg · Pull Request #3751 · pingdotgg/t3code · GitHub
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
190 changes: 190 additions & 0 deletions apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2947,6 +2947,196 @@ describe("ProviderRuntimeIngestion", () => {
).toBe("# Plan title");
});

it("titles task activities with the task description, including on completion", async () => {
const harness = await createHarness();
const now = "2026-01-01T00:00:00.000Z";

harness.emit({
type: "task.started",
eventId: asEventId("evt-named-task-started"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-named-task"),
payload: {
taskId: "named-task-1",
description: "Typecheck mobile app",
taskType: "local_bash",
},
});

harness.emit({
type: "task.progress",
eventId: asEventId("evt-named-task-progress"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-named-task"),
payload: {
taskId: "named-task-1",
description: "Typecheck mobile app",
summary: "Running tsc across the mobile workspace.",
},
});

harness.emit({
type: "task.completed",
eventId: asEventId("evt-named-task-completed"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-named-task"),
payload: {
taskId: "named-task-1",
status: "completed",
summary: "Typecheck finished without errors.",
},
});

const thread = await waitForThread(harness.readModel, (entry) =>
entry.activities.some(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-named-task-completed",
),
);

const progress = thread.activities.find(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-named-task-progress",
);
const completed = thread.activities.find(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-named-task-completed",
);

const progressPayload =
progress?.payload && typeof progress.payload === "object"
? (progress.payload as Record<string, unknown>)
: undefined;
const completedPayload =
completed?.payload && typeof completed.payload === "object"
? (completed.payload as Record<string, unknown>)
: undefined;

expect(progress?.summary).toBe("Typecheck mobile app");
expect(progressPayload?.title).toBe("Typecheck mobile app");
expect(completed?.summary).toBe("Task completed");
expect(completedPayload?.title).toBe("Typecheck mobile app");
expect(completedPayload?.summary).toBe("Typecheck finished without errors.");
expect(completedPayload?.detail).toBe("Typecheck finished without errors.");
});

it("titles task completion from task.started when no progress event carried the name", async () => {
const harness = await createHarness();
const now = "2026-01-01T00:00:00.000Z";

harness.emit({
type: "task.started",
eventId: asEventId("evt-fast-task-started"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-fast-task"),
payload: {
taskId: "fast-task-1",
description: "wait for codex review to finish",
taskType: "local_bash",
},
});

harness.emit({
type: "task.completed",
eventId: asEventId("evt-fast-task-completed"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-fast-task"),
payload: {
taskId: "fast-task-1",
status: "completed",
},
});

const thread = await waitForThread(harness.readModel, (entry) =>
entry.activities.some(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-fast-task-completed",
),
);

const completed = thread.activities.find(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-fast-task-completed",
);
const completedPayload =
completed?.payload && typeof completed.payload === "object"
? (completed.payload as Record<string, unknown>)
: undefined;

expect(completedPayload?.title).toBe("wait for codex review to finish");
});

it("titles task completion from persisted activities after the description cache is swept", async () => {
const harness = await createHarness();
const now = "2026-01-01T00:00:00.000Z";

harness.emit({
type: "task.progress",
eventId: asEventId("evt-swept-task-progress"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-swept-task"),
payload: {
taskId: "swept-task-1",
description: "Watch round-3 CI and bots",
summary: "Polling CI checks.",
},
});

await waitForThread(harness.readModel, (entry) =>
entry.activities.some(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-swept-task-progress",
),
);

// session.exited sweeps the in-memory description cache; the completion
// that follows must recover the name from persisted activities.
harness.emit({
type: "session.exited",
eventId: asEventId("evt-swept-task-session-exited"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
payload: {},
});

harness.emit({
type: "task.completed",
eventId: asEventId("evt-swept-task-completed"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-swept-task"),
payload: {
taskId: "swept-task-1",
status: "completed",
summary: "CI is green.",
},
});

const thread = await waitForThread(harness.readModel, (entry) =>
entry.activities.some(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-swept-task-completed",
),
);

const completed = thread.activities.find(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-swept-task-completed",
);
const completedPayload =
completed?.payload && typeof completed.payload === "object"
? (completed.payload as Record<string, unknown>)
: undefined;

expect(completedPayload?.title).toBe("Watch round-3 CI and bots");
});

it("projects structured user input request and resolution as thread activities", async () => {
const harness = await createHarness();
const now = "2026-01-01T00:00:00.000Z";
Expand Down
102 changes: 99 additions & 3 deletions apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,42 @@ import {
import { ServerSettingsService } from "../../serverSettings.ts";

const providerTurnKey = (threadId: ThreadId, turnId: TurnId) => `${threadId}:${turnId}`;
const providerTaskKey = (threadId: ThreadId, taskId: string) => `${threadId}:${taskId}`;

// Fallback when the in-memory description cache no longer has the task name
// (server restart, session-exit sweep, TTL/capacity eviction): earlier
// task.started/task.progress activities for the task are persisted with it.
function findTaskTitleInActivities(
activities: ReadonlyArray<OrchestrationThreadActivity> | undefined,
taskId: string,
): string | undefined {
if (!activities) {
return undefined;
}
for (let index = activities.length - 1; index >= 0; index -= 1) {
const activity = activities[index];
if (!activity || (activity.kind !== "task.started" && activity.kind !== "task.progress")) {
continue;
}
const payload =
activity.payload && typeof activity.payload === "object"
? (activity.payload as { taskId?: unknown; title?: unknown; detail?: unknown })
: undefined;
if (payload?.taskId !== taskId) {
continue;
}
const title =
typeof payload.title === "string"
? payload.title
: activity.kind === "task.started" && typeof payload.detail === "string"
? payload.detail
: undefined;
if (title && title.trim().length > 0) {
return title;
}
}
return undefined;
}

interface AssistantSegmentState {
baseKey: string;
Expand All@@ -53,6 +89,8 @@ const BUFFERED_MESSAGE_TEXT_BY_MESSAGE_ID_CACHE_CAPACITY = 20_000;
const BUFFERED_MESSAGE_TEXT_BY_MESSAGE_ID_TTL = Duration.minutes(120);
const BUFFERED_PROPOSED_PLAN_BY_ID_CACHE_CAPACITY = 10_000;
const BUFFERED_PROPOSED_PLAN_BY_ID_TTL = Duration.minutes(120);
const TASK_DESCRIPTION_BY_TASK_CACHE_CAPACITY = 10_000;
const TASK_DESCRIPTION_BY_TASK_TTL = Duration.minutes(120);
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
const MAX_BUFFERED_ASSISTANT_CHARS = 24_000;
const STRICT_PROVIDER_LIFECYCLE_GUARD = process.env.T3CODE_STRICT_PROVIDER_LIFECYCLE_GUARD !== "0";

Expand DownExpand Up@@ -264,6 +302,7 @@ function requestKindFromCanonicalRequestType(

function runtimeEventToActivities(
event: ProviderRuntimeEvent,
taskTitle?: string,
): ReadonlyArray<OrchestrationThreadActivity> {
const maybeSequence = (() => {
const eventWithSequence = event as ProviderRuntimeEvent & { sessionSequence?: number };
Expand DownExpand Up@@ -473,9 +512,15 @@ function runtimeEventToActivities(
createdAt: event.createdAt,
tone: "info",
kind: "task.progress",
summary: "Reasoning update",
summary:
event.payload.description.trim().length > 0
? truncateDetail(event.payload.description, 120)
: "Reasoning update",
payload: {
taskId: event.payload.taskId,
...(event.payload.description.trim().length > 0
? { title: truncateDetail(event.payload.description, 120) }
: {}),
detail: truncateDetail(event.payload.summary ?? event.payload.description),
...(event.payload.summary ? { summary: truncateDetail(event.payload.summary) } : {}),
...(event.payload.lastToolName ? { lastToolName: event.payload.lastToolName } : {}),
Expand DownExpand Up@@ -503,7 +548,15 @@ function runtimeEventToActivities(
payload: {
taskId: event.payload.taskId,
status: event.payload.status,
...(event.payload.summary ? { detail: truncateDetail(event.payload.summary) } : {}),
...(taskTitle ? { title: truncateDetail(taskTitle, 120) } : {}),
// summary + detail mirror task.progress: clients label the row from
// summary and keep detail for the preview/expanded body.
...(event.payload.summary
? {
summary: truncateDetail(event.payload.summary),
detail: truncateDetail(event.payload.summary),
}
: {}),
...(event.payload.usage !== undefined ? { usage: event.payload.usage } : {}),
},
turnId: toTurnId(event.turnId) ?? null,
Expand DownExpand Up@@ -666,6 +719,27 @@ const make = Effect.gen(function* () {
lookup: () => Effect.succeed({ text: "", createdAt: "" }),
});

// Task names arrive on task.started/task.progress but not on task.completed,
// so remember them per task to title the completion activity.
const taskDescriptionByTaskKey = yield* Cache.make<string, string>({
capacity: TASK_DESCRIPTION_BY_TASK_CACHE_CAPACITY,
timeToLive: TASK_DESCRIPTION_BY_TASK_TTL,
lookup: () => Effect.succeed(""),
});

const rememberTaskDescription = (threadId: ThreadId, taskId: string, description: string) =>
Cache.set(taskDescriptionByTaskKey, providerTaskKey(threadId, taskId), description);

// Entries are left in place after completion so replayed or duplicate
// terminal events stay titled; TTL, capacity, and the session-exit sweep
// bound the cache.
const lookupTaskDescription = (threadId: ThreadId, taskId: string) =>
Cache.getOption(taskDescriptionByTaskKey, providerTaskKey(threadId, taskId)).pipe(
Effect.map((description) =>
Option.filter(description, (value) => value.length > 0).pipe(Option.getOrUndefined),
),
);

const resolveThreadDetail = Effect.fn("resolveThreadDetail")(function* (threadId: ThreadId) {
return yield* projectionSnapshotQuery
.getThreadDetailById(threadId)
Expand DownExpand Up@@ -1090,6 +1164,7 @@ const make = Effect.gen(function* () {
const turnKeys = Array.from(yield* Cache.keys(turnMessageIdsByTurnKey));
const assistantSegmentKeys = Array.from(yield* Cache.keys(assistantSegmentStateByTurnKey));
const proposedPlanKeys = Array.from(yield* Cache.keys(bufferedProposedPlanById));
const taskDescriptionKeys = Array.from(yield* Cache.keys(taskDescriptionByTaskKey));
yield* Effect.forEach(
turnKeys,
(key) =>
Expand DownExpand Up@@ -1125,6 +1200,12 @@ const make = Effect.gen(function* () {
: Effect.void,
{ concurrency: 1 },
).pipe(Effect.asVoid);
yield* Effect.forEach(
taskDescriptionKeys,
(key) =>
key.startsWith(prefix) ? Cache.invalidate(taskDescriptionByTaskKey, key) : Effect.void,
{ concurrency: 1 },
).pipe(Effect.asVoid);
});

const getSourceProposedPlanReferenceForPendingTurnStart = Effect.fn(
Expand DownExpand Up@@ -1654,7 +1735,22 @@ const make = Effect.gen(function* () {
}
}

const activities = runtimeEventToActivities(event);
if (event.type === "task.started" || event.type === "task.progress") {
const description = event.payload.description?.trim();
if (description) {
yield* rememberTaskDescription(thread.id, event.payload.taskId, description);
}
}
let taskTitle: string | undefined;
if (event.type === "task.completed") {
taskTitle = yield* lookupTaskDescription(thread.id, event.payload.taskId);
if (!taskTitle) {
const threadDetail = yield* getLoadedThreadDetail();
taskTitle = findTaskTitleInActivities(threadDetail?.activities, event.payload.taskId);
}
}

const activities = runtimeEventToActivities(event, taskTitle);
yield* Effect.forEach(activities, (activity) =>
providerCommandId(event, "thread-activity-append").pipe(
Effect.flatMap((commandId) =>
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' feat(server): title background-task work-log rows with the task name by t3dotgg · Pull Request #3751 · pingdotgg/t3code · GitHub
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
190 changes: 190 additions & 0 deletions apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2947,6 +2947,196 @@ describe("ProviderRuntimeIngestion", () => {
).toBe("# Plan title");
});

it("titles task activities with the task description, including on completion", async () => {
const harness = await createHarness();
const now = "2026-01-01T00:00:00.000Z";

harness.emit({
type: "task.started",
eventId: asEventId("evt-named-task-started"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-named-task"),
payload: {
taskId: "named-task-1",
description: "Typecheck mobile app",
taskType: "local_bash",
},
});

harness.emit({
type: "task.progress",
eventId: asEventId("evt-named-task-progress"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-named-task"),
payload: {
taskId: "named-task-1",
description: "Typecheck mobile app",
summary: "Running tsc across the mobile workspace.",
},
});

harness.emit({
type: "task.completed",
eventId: asEventId("evt-named-task-completed"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-named-task"),
payload: {
taskId: "named-task-1",
status: "completed",
summary: "Typecheck finished without errors.",
},
});

const thread = await waitForThread(harness.readModel, (entry) =>
entry.activities.some(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-named-task-completed",
),
);

const progress = thread.activities.find(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-named-task-progress",
);
const completed = thread.activities.find(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-named-task-completed",
);

const progressPayload =
progress?.payload && typeof progress.payload === "object"
? (progress.payload as Record<string, unknown>)
: undefined;
const completedPayload =
completed?.payload && typeof completed.payload === "object"
? (completed.payload as Record<string, unknown>)
: undefined;

expect(progress?.summary).toBe("Typecheck mobile app");
expect(progressPayload?.title).toBe("Typecheck mobile app");
expect(completed?.summary).toBe("Task completed");
expect(completedPayload?.title).toBe("Typecheck mobile app");
expect(completedPayload?.summary).toBe("Typecheck finished without errors.");
expect(completedPayload?.detail).toBe("Typecheck finished without errors.");
});

it("titles task completion from task.started when no progress event carried the name", async () => {
const harness = await createHarness();
const now = "2026-01-01T00:00:00.000Z";

harness.emit({
type: "task.started",
eventId: asEventId("evt-fast-task-started"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-fast-task"),
payload: {
taskId: "fast-task-1",
description: "wait for codex review to finish",
taskType: "local_bash",
},
});

harness.emit({
type: "task.completed",
eventId: asEventId("evt-fast-task-completed"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-fast-task"),
payload: {
taskId: "fast-task-1",
status: "completed",
},
});

const thread = await waitForThread(harness.readModel, (entry) =>
entry.activities.some(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-fast-task-completed",
),
);

const completed = thread.activities.find(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-fast-task-completed",
);
const completedPayload =
completed?.payload && typeof completed.payload === "object"
? (completed.payload as Record<string, unknown>)
: undefined;

expect(completedPayload?.title).toBe("wait for codex review to finish");
});

it("titles task completion from persisted activities after the description cache is swept", async () => {
const harness = await createHarness();
const now = "2026-01-01T00:00:00.000Z";

harness.emit({
type: "task.progress",
eventId: asEventId("evt-swept-task-progress"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-swept-task"),
payload: {
taskId: "swept-task-1",
description: "Watch round-3 CI and bots",
summary: "Polling CI checks.",
},
});

await waitForThread(harness.readModel, (entry) =>
entry.activities.some(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-swept-task-progress",
),
);

// session.exited sweeps the in-memory description cache; the completion
// that follows must recover the name from persisted activities.
harness.emit({
type: "session.exited",
eventId: asEventId("evt-swept-task-session-exited"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
payload: {},
});

harness.emit({
type: "task.completed",
eventId: asEventId("evt-swept-task-completed"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-swept-task"),
payload: {
taskId: "swept-task-1",
status: "completed",
summary: "CI is green.",
},
});

const thread = await waitForThread(harness.readModel, (entry) =>
entry.activities.some(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-swept-task-completed",
),
);

const completed = thread.activities.find(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-swept-task-completed",
);
const completedPayload =
completed?.payload && typeof completed.payload === "object"
? (completed.payload as Record<string, unknown>)
: undefined;

expect(completedPayload?.title).toBe("Watch round-3 CI and bots");
});

it("projects structured user input request and resolution as thread activities", async () => {
const harness = await createHarness();
const now = "2026-01-01T00:00:00.000Z";
Expand Down
102 changes: 99 additions & 3 deletions apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,42 @@ import {
import { ServerSettingsService } from "../../serverSettings.ts";

const providerTurnKey = (threadId: ThreadId, turnId: TurnId) => `${threadId}:${turnId}`;
const providerTaskKey = (threadId: ThreadId, taskId: string) => `${threadId}:${taskId}`;

// Fallback when the in-memory description cache no longer has the task name
// (server restart, session-exit sweep, TTL/capacity eviction): earlier
// task.started/task.progress activities for the task are persisted with it.
function findTaskTitleInActivities(
activities: ReadonlyArray<OrchestrationThreadActivity> | undefined,
taskId: string,
): string | undefined {
if (!activities) {
return undefined;
}
for (let index = activities.length - 1; index >= 0; index -= 1) {
const activity = activities[index];
if (!activity || (activity.kind !== "task.started" && activity.kind !== "task.progress")) {
continue;
}
const payload =
activity.payload && typeof activity.payload === "object"
? (activity.payload as { taskId?: unknown; title?: unknown; detail?: unknown })
: undefined;
if (payload?.taskId !== taskId) {
continue;
}
const title =
typeof payload.title === "string"
? payload.title
: activity.kind === "task.started" && typeof payload.detail === "string"
? payload.detail
: undefined;
if (title && title.trim().length > 0) {
return title;
}
}
return undefined;
}

interface AssistantSegmentState {
baseKey: string;
Expand All@@ -53,6 +89,8 @@ const BUFFERED_MESSAGE_TEXT_BY_MESSAGE_ID_CACHE_CAPACITY = 20_000;
const BUFFERED_MESSAGE_TEXT_BY_MESSAGE_ID_TTL = Duration.minutes(120);
const BUFFERED_PROPOSED_PLAN_BY_ID_CACHE_CAPACITY = 10_000;
const BUFFERED_PROPOSED_PLAN_BY_ID_TTL = Duration.minutes(120);
const TASK_DESCRIPTION_BY_TASK_CACHE_CAPACITY = 10_000;
const TASK_DESCRIPTION_BY_TASK_TTL = Duration.minutes(120);
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
const MAX_BUFFERED_ASSISTANT_CHARS = 24_000;
const STRICT_PROVIDER_LIFECYCLE_GUARD = process.env.T3CODE_STRICT_PROVIDER_LIFECYCLE_GUARD !== "0";

Expand DownExpand Up@@ -264,6 +302,7 @@ function requestKindFromCanonicalRequestType(

function runtimeEventToActivities(
event: ProviderRuntimeEvent,
taskTitle?: string,
): ReadonlyArray<OrchestrationThreadActivity> {
const maybeSequence = (() => {
const eventWithSequence = event as ProviderRuntimeEvent & { sessionSequence?: number };
Expand DownExpand Up@@ -473,9 +512,15 @@ function runtimeEventToActivities(
createdAt: event.createdAt,
tone: "info",
kind: "task.progress",
summary: "Reasoning update",
summary:
event.payload.description.trim().length > 0
? truncateDetail(event.payload.description, 120)
: "Reasoning update",
payload: {
taskId: event.payload.taskId,
...(event.payload.description.trim().length > 0
? { title: truncateDetail(event.payload.description, 120) }
: {}),
detail: truncateDetail(event.payload.summary ?? event.payload.description),
...(event.payload.summary ? { summary: truncateDetail(event.payload.summary) } : {}),
...(event.payload.lastToolName ? { lastToolName: event.payload.lastToolName } : {}),
Expand DownExpand Up@@ -503,7 +548,15 @@ function runtimeEventToActivities(
payload: {
taskId: event.payload.taskId,
status: event.payload.status,
...(event.payload.summary ? { detail: truncateDetail(event.payload.summary) } : {}),
...(taskTitle ? { title: truncateDetail(taskTitle, 120) } : {}),
// summary + detail mirror task.progress: clients label the row from
// summary and keep detail for the preview/expanded body.
...(event.payload.summary
? {
summary: truncateDetail(event.payload.summary),
detail: truncateDetail(event.payload.summary),
}
: {}),
...(event.payload.usage !== undefined ? { usage: event.payload.usage } : {}),
},
turnId: toTurnId(event.turnId) ?? null,
Expand DownExpand Up@@ -666,6 +719,27 @@ const make = Effect.gen(function* () {
lookup: () => Effect.succeed({ text: "", createdAt: "" }),
});

// Task names arrive on task.started/task.progress but not on task.completed,
// so remember them per task to title the completion activity.
const taskDescriptionByTaskKey = yield* Cache.make<string, string>({
capacity: TASK_DESCRIPTION_BY_TASK_CACHE_CAPACITY,
timeToLive: TASK_DESCRIPTION_BY_TASK_TTL,
lookup: () => Effect.succeed(""),
});

const rememberTaskDescription = (threadId: ThreadId, taskId: string, description: string) =>
Cache.set(taskDescriptionByTaskKey, providerTaskKey(threadId, taskId), description);

// Entries are left in place after completion so replayed or duplicate
// terminal events stay titled; TTL, capacity, and the session-exit sweep
// bound the cache.
const lookupTaskDescription = (threadId: ThreadId, taskId: string) =>
Cache.getOption(taskDescriptionByTaskKey, providerTaskKey(threadId, taskId)).pipe(
Effect.map((description) =>
Option.filter(description, (value) => value.length > 0).pipe(Option.getOrUndefined),
),
);

const resolveThreadDetail = Effect.fn("resolveThreadDetail")(function* (threadId: ThreadId) {
return yield* projectionSnapshotQuery
.getThreadDetailById(threadId)
Expand DownExpand Up@@ -1090,6 +1164,7 @@ const make = Effect.gen(function* () {
const turnKeys = Array.from(yield* Cache.keys(turnMessageIdsByTurnKey));
const assistantSegmentKeys = Array.from(yield* Cache.keys(assistantSegmentStateByTurnKey));
const proposedPlanKeys = Array.from(yield* Cache.keys(bufferedProposedPlanById));
const taskDescriptionKeys = Array.from(yield* Cache.keys(taskDescriptionByTaskKey));
yield* Effect.forEach(
turnKeys,
(key) =>
Expand DownExpand Up@@ -1125,6 +1200,12 @@ const make = Effect.gen(function* () {
: Effect.void,
{ concurrency: 1 },
).pipe(Effect.asVoid);
yield* Effect.forEach(
taskDescriptionKeys,
(key) =>
key.startsWith(prefix) ? Cache.invalidate(taskDescriptionByTaskKey, key) : Effect.void,
{ concurrency: 1 },
).pipe(Effect.asVoid);
});

const getSourceProposedPlanReferenceForPendingTurnStart = Effect.fn(
Expand DownExpand Up@@ -1654,7 +1735,22 @@ const make = Effect.gen(function* () {
}
}

const activities = runtimeEventToActivities(event);
if (event.type === "task.started" || event.type === "task.progress") {
const description = event.payload.description?.trim();
if (description) {
yield* rememberTaskDescription(thread.id, event.payload.taskId, description);
}
}
let taskTitle: string | undefined;
if (event.type === "task.completed") {
taskTitle = yield* lookupTaskDescription(thread.id, event.payload.taskId);
if (!taskTitle) {
const threadDetail = yield* getLoadedThreadDetail();
taskTitle = findTaskTitleInActivities(threadDetail?.activities, event.payload.taskId);
}
}

const activities = runtimeEventToActivities(event, taskTitle);
yield* Effect.forEach(activities, (activity) =>
providerCommandId(event, "thread-activity-append").pipe(
Effect.flatMap((commandId) =>
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(server): title background-task work-log rows with the task name by t3dotgg · Pull Request #3751 · pingdotgg/t3code · GitHub
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
190 changes: 190 additions & 0 deletions apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2947,6 +2947,196 @@ describe("ProviderRuntimeIngestion", () => {
).toBe("# Plan title");
});

it("titles task activities with the task description, including on completion", async () => {
const harness = await createHarness();
const now = "2026-01-01T00:00:00.000Z";

harness.emit({
type: "task.started",
eventId: asEventId("evt-named-task-started"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-named-task"),
payload: {
taskId: "named-task-1",
description: "Typecheck mobile app",
taskType: "local_bash",
},
});

harness.emit({
type: "task.progress",
eventId: asEventId("evt-named-task-progress"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-named-task"),
payload: {
taskId: "named-task-1",
description: "Typecheck mobile app",
summary: "Running tsc across the mobile workspace.",
},
});

harness.emit({
type: "task.completed",
eventId: asEventId("evt-named-task-completed"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-named-task"),
payload: {
taskId: "named-task-1",
status: "completed",
summary: "Typecheck finished without errors.",
},
});

const thread = await waitForThread(harness.readModel, (entry) =>
entry.activities.some(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-named-task-completed",
),
);

const progress = thread.activities.find(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-named-task-progress",
);
const completed = thread.activities.find(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-named-task-completed",
);

const progressPayload =
progress?.payload && typeof progress.payload === "object"
? (progress.payload as Record<string, unknown>)
: undefined;
const completedPayload =
completed?.payload && typeof completed.payload === "object"
? (completed.payload as Record<string, unknown>)
: undefined;

expect(progress?.summary).toBe("Typecheck mobile app");
expect(progressPayload?.title).toBe("Typecheck mobile app");
expect(completed?.summary).toBe("Task completed");
expect(completedPayload?.title).toBe("Typecheck mobile app");
expect(completedPayload?.summary).toBe("Typecheck finished without errors.");
expect(completedPayload?.detail).toBe("Typecheck finished without errors.");
});

it("titles task completion from task.started when no progress event carried the name", async () => {
const harness = await createHarness();
const now = "2026-01-01T00:00:00.000Z";

harness.emit({
type: "task.started",
eventId: asEventId("evt-fast-task-started"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-fast-task"),
payload: {
taskId: "fast-task-1",
description: "wait for codex review to finish",
taskType: "local_bash",
},
});

harness.emit({
type: "task.completed",
eventId: asEventId("evt-fast-task-completed"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-fast-task"),
payload: {
taskId: "fast-task-1",
status: "completed",
},
});

const thread = await waitForThread(harness.readModel, (entry) =>
entry.activities.some(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-fast-task-completed",
),
);

const completed = thread.activities.find(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-fast-task-completed",
);
const completedPayload =
completed?.payload && typeof completed.payload === "object"
? (completed.payload as Record<string, unknown>)
: undefined;

expect(completedPayload?.title).toBe("wait for codex review to finish");
});

it("titles task completion from persisted activities after the description cache is swept", async () => {
const harness = await createHarness();
const now = "2026-01-01T00:00:00.000Z";

harness.emit({
type: "task.progress",
eventId: asEventId("evt-swept-task-progress"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-swept-task"),
payload: {
taskId: "swept-task-1",
description: "Watch round-3 CI and bots",
summary: "Polling CI checks.",
},
});

await waitForThread(harness.readModel, (entry) =>
entry.activities.some(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-swept-task-progress",
),
);

// session.exited sweeps the in-memory description cache; the completion
// that follows must recover the name from persisted activities.
harness.emit({
type: "session.exited",
eventId: asEventId("evt-swept-task-session-exited"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
payload: {},
});

harness.emit({
type: "task.completed",
eventId: asEventId("evt-swept-task-completed"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-swept-task"),
payload: {
taskId: "swept-task-1",
status: "completed",
summary: "CI is green.",
},
});

const thread = await waitForThread(harness.readModel, (entry) =>
entry.activities.some(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-swept-task-completed",
),
);

const completed = thread.activities.find(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-swept-task-completed",
);
const completedPayload =
completed?.payload && typeof completed.payload === "object"
? (completed.payload as Record<string, unknown>)
: undefined;

expect(completedPayload?.title).toBe("Watch round-3 CI and bots");
});

it("projects structured user input request and resolution as thread activities", async () => {
const harness = await createHarness();
const now = "2026-01-01T00:00:00.000Z";
Expand Down
102 changes: 99 additions & 3 deletions apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,42 @@ import {
import { ServerSettingsService } from "../../serverSettings.ts";

const providerTurnKey = (threadId: ThreadId, turnId: TurnId) => `${threadId}:${turnId}`;
const providerTaskKey = (threadId: ThreadId, taskId: string) => `${threadId}:${taskId}`;

// Fallback when the in-memory description cache no longer has the task name
// (server restart, session-exit sweep, TTL/capacity eviction): earlier
// task.started/task.progress activities for the task are persisted with it.
function findTaskTitleInActivities(
activities: ReadonlyArray<OrchestrationThreadActivity> | undefined,
taskId: string,
): string | undefined {
if (!activities) {
return undefined;
}
for (let index = activities.length - 1; index >= 0; index -= 1) {
const activity = activities[index];
if (!activity || (activity.kind !== "task.started" && activity.kind !== "task.progress")) {
continue;
}
const payload =
activity.payload && typeof activity.payload === "object"
? (activity.payload as { taskId?: unknown; title?: unknown; detail?: unknown })
: undefined;
if (payload?.taskId !== taskId) {
continue;
}
const title =
typeof payload.title === "string"
? payload.title
: activity.kind === "task.started" && typeof payload.detail === "string"
? payload.detail
: undefined;
if (title && title.trim().length > 0) {
return title;
}
}
return undefined;
}

interface AssistantSegmentState {
baseKey: string;
Expand All@@ -53,6 +89,8 @@ const BUFFERED_MESSAGE_TEXT_BY_MESSAGE_ID_CACHE_CAPACITY = 20_000;
const BUFFERED_MESSAGE_TEXT_BY_MESSAGE_ID_TTL = Duration.minutes(120);
const BUFFERED_PROPOSED_PLAN_BY_ID_CACHE_CAPACITY = 10_000;
const BUFFERED_PROPOSED_PLAN_BY_ID_TTL = Duration.minutes(120);
const TASK_DESCRIPTION_BY_TASK_CACHE_CAPACITY = 10_000;
const TASK_DESCRIPTION_BY_TASK_TTL = Duration.minutes(120);
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
const MAX_BUFFERED_ASSISTANT_CHARS = 24_000;
const STRICT_PROVIDER_LIFECYCLE_GUARD = process.env.T3CODE_STRICT_PROVIDER_LIFECYCLE_GUARD !== "0";

Expand DownExpand Up@@ -264,6 +302,7 @@ function requestKindFromCanonicalRequestType(

function runtimeEventToActivities(
event: ProviderRuntimeEvent,
taskTitle?: string,
): ReadonlyArray<OrchestrationThreadActivity> {
const maybeSequence = (() => {
const eventWithSequence = event as ProviderRuntimeEvent & { sessionSequence?: number };
Expand DownExpand Up@@ -473,9 +512,15 @@ function runtimeEventToActivities(
createdAt: event.createdAt,
tone: "info",
kind: "task.progress",
summary: "Reasoning update",
summary:
event.payload.description.trim().length > 0
? truncateDetail(event.payload.description, 120)
: "Reasoning update",
payload: {
taskId: event.payload.taskId,
...(event.payload.description.trim().length > 0
? { title: truncateDetail(event.payload.description, 120) }
: {}),
detail: truncateDetail(event.payload.summary ?? event.payload.description),
...(event.payload.summary ? { summary: truncateDetail(event.payload.summary) } : {}),
...(event.payload.lastToolName ? { lastToolName: event.payload.lastToolName } : {}),
Expand DownExpand Up@@ -503,7 +548,15 @@ function runtimeEventToActivities(
payload: {
taskId: event.payload.taskId,
status: event.payload.status,
...(event.payload.summary ? { detail: truncateDetail(event.payload.summary) } : {}),
...(taskTitle ? { title: truncateDetail(taskTitle, 120) } : {}),
// summary + detail mirror task.progress: clients label the row from
// summary and keep detail for the preview/expanded body.
...(event.payload.summary
? {
summary: truncateDetail(event.payload.summary),
detail: truncateDetail(event.payload.summary),
}
: {}),
...(event.payload.usage !== undefined ? { usage: event.payload.usage } : {}),
},
turnId: toTurnId(event.turnId) ?? null,
Expand DownExpand Up@@ -666,6 +719,27 @@ const make = Effect.gen(function* () {
lookup: () => Effect.succeed({ text: "", createdAt: "" }),
});

// Task names arrive on task.started/task.progress but not on task.completed,
// so remember them per task to title the completion activity.
const taskDescriptionByTaskKey = yield* Cache.make<string, string>({
capacity: TASK_DESCRIPTION_BY_TASK_CACHE_CAPACITY,
timeToLive: TASK_DESCRIPTION_BY_TASK_TTL,
lookup: () => Effect.succeed(""),
});

const rememberTaskDescription = (threadId: ThreadId, taskId: string, description: string) =>
Cache.set(taskDescriptionByTaskKey, providerTaskKey(threadId, taskId), description);

// Entries are left in place after completion so replayed or duplicate
// terminal events stay titled; TTL, capacity, and the session-exit sweep
// bound the cache.
const lookupTaskDescription = (threadId: ThreadId, taskId: string) =>
Cache.getOption(taskDescriptionByTaskKey, providerTaskKey(threadId, taskId)).pipe(
Effect.map((description) =>
Option.filter(description, (value) => value.length > 0).pipe(Option.getOrUndefined),
),
);

const resolveThreadDetail = Effect.fn("resolveThreadDetail")(function* (threadId: ThreadId) {
return yield* projectionSnapshotQuery
.getThreadDetailById(threadId)
Expand DownExpand Up@@ -1090,6 +1164,7 @@ const make = Effect.gen(function* () {
const turnKeys = Array.from(yield* Cache.keys(turnMessageIdsByTurnKey));
const assistantSegmentKeys = Array.from(yield* Cache.keys(assistantSegmentStateByTurnKey));
const proposedPlanKeys = Array.from(yield* Cache.keys(bufferedProposedPlanById));
const taskDescriptionKeys = Array.from(yield* Cache.keys(taskDescriptionByTaskKey));
yield* Effect.forEach(
turnKeys,
(key) =>
Expand DownExpand Up@@ -1125,6 +1200,12 @@ const make = Effect.gen(function* () {
: Effect.void,
{ concurrency: 1 },
).pipe(Effect.asVoid);
yield* Effect.forEach(
taskDescriptionKeys,
(key) =>
key.startsWith(prefix) ? Cache.invalidate(taskDescriptionByTaskKey, key) : Effect.void,
{ concurrency: 1 },
).pipe(Effect.asVoid);
});

const getSourceProposedPlanReferenceForPendingTurnStart = Effect.fn(
Expand DownExpand Up@@ -1654,7 +1735,22 @@ const make = Effect.gen(function* () {
}
}

const activities = runtimeEventToActivities(event);
if (event.type === "task.started" || event.type === "task.progress") {
const description = event.payload.description?.trim();
if (description) {
yield* rememberTaskDescription(thread.id, event.payload.taskId, description);
}
}
let taskTitle: string | undefined;
if (event.type === "task.completed") {
taskTitle = yield* lookupTaskDescription(thread.id, event.payload.taskId);
if (!taskTitle) {
const threadDetail = yield* getLoadedThreadDetail();
taskTitle = findTaskTitleInActivities(threadDetail?.activities, event.payload.taskId);
}
}

const activities = runtimeEventToActivities(event, taskTitle);
yield* Effect.forEach(activities, (activity) =>
providerCommandId(event, "thread-activity-append").pipe(
Effect.flatMap((commandId) =>
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(server): title background-task work-log rows with the task name by t3dotgg · Pull Request #3751 · pingdotgg/t3code · GitHub
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
190 changes: 190 additions & 0 deletions apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2947,6 +2947,196 @@ describe("ProviderRuntimeIngestion", () => {
).toBe("# Plan title");
});

it("titles task activities with the task description, including on completion", async () => {
const harness = await createHarness();
const now = "2026-01-01T00:00:00.000Z";

harness.emit({
type: "task.started",
eventId: asEventId("evt-named-task-started"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-named-task"),
payload: {
taskId: "named-task-1",
description: "Typecheck mobile app",
taskType: "local_bash",
},
});

harness.emit({
type: "task.progress",
eventId: asEventId("evt-named-task-progress"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-named-task"),
payload: {
taskId: "named-task-1",
description: "Typecheck mobile app",
summary: "Running tsc across the mobile workspace.",
},
});

harness.emit({
type: "task.completed",
eventId: asEventId("evt-named-task-completed"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-named-task"),
payload: {
taskId: "named-task-1",
status: "completed",
summary: "Typecheck finished without errors.",
},
});

const thread = await waitForThread(harness.readModel, (entry) =>
entry.activities.some(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-named-task-completed",
),
);

const progress = thread.activities.find(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-named-task-progress",
);
const completed = thread.activities.find(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-named-task-completed",
);

const progressPayload =
progress?.payload && typeof progress.payload === "object"
? (progress.payload as Record<string, unknown>)
: undefined;
const completedPayload =
completed?.payload && typeof completed.payload === "object"
? (completed.payload as Record<string, unknown>)
: undefined;

expect(progress?.summary).toBe("Typecheck mobile app");
expect(progressPayload?.title).toBe("Typecheck mobile app");
expect(completed?.summary).toBe("Task completed");
expect(completedPayload?.title).toBe("Typecheck mobile app");
expect(completedPayload?.summary).toBe("Typecheck finished without errors.");
expect(completedPayload?.detail).toBe("Typecheck finished without errors.");
});

it("titles task completion from task.started when no progress event carried the name", async () => {
const harness = await createHarness();
const now = "2026-01-01T00:00:00.000Z";

harness.emit({
type: "task.started",
eventId: asEventId("evt-fast-task-started"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-fast-task"),
payload: {
taskId: "fast-task-1",
description: "wait for codex review to finish",
taskType: "local_bash",
},
});

harness.emit({
type: "task.completed",
eventId: asEventId("evt-fast-task-completed"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-fast-task"),
payload: {
taskId: "fast-task-1",
status: "completed",
},
});

const thread = await waitForThread(harness.readModel, (entry) =>
entry.activities.some(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-fast-task-completed",
),
);

const completed = thread.activities.find(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-fast-task-completed",
);
const completedPayload =
completed?.payload && typeof completed.payload === "object"
? (completed.payload as Record<string, unknown>)
: undefined;

expect(completedPayload?.title).toBe("wait for codex review to finish");
});

it("titles task completion from persisted activities after the description cache is swept", async () => {
const harness = await createHarness();
const now = "2026-01-01T00:00:00.000Z";

harness.emit({
type: "task.progress",
eventId: asEventId("evt-swept-task-progress"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-swept-task"),
payload: {
taskId: "swept-task-1",
description: "Watch round-3 CI and bots",
summary: "Polling CI checks.",
},
});

await waitForThread(harness.readModel, (entry) =>
entry.activities.some(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-swept-task-progress",
),
);

// session.exited sweeps the in-memory description cache; the completion
// that follows must recover the name from persisted activities.
harness.emit({
type: "session.exited",
eventId: asEventId("evt-swept-task-session-exited"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
payload: {},
});

harness.emit({
type: "task.completed",
eventId: asEventId("evt-swept-task-completed"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-swept-task"),
payload: {
taskId: "swept-task-1",
status: "completed",
summary: "CI is green.",
},
});

const thread = await waitForThread(harness.readModel, (entry) =>
entry.activities.some(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-swept-task-completed",
),
);

const completed = thread.activities.find(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-swept-task-completed",
);
const completedPayload =
completed?.payload && typeof completed.payload === "object"
? (completed.payload as Record<string, unknown>)
: undefined;

expect(completedPayload?.title).toBe("Watch round-3 CI and bots");
});

it("projects structured user input request and resolution as thread activities", async () => {
const harness = await createHarness();
const now = "2026-01-01T00:00:00.000Z";
Expand Down
102 changes: 99 additions & 3 deletions apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,42 @@ import {
import { ServerSettingsService } from "../../serverSettings.ts";

const providerTurnKey = (threadId: ThreadId, turnId: TurnId) => `${threadId}:${turnId}`;
const providerTaskKey = (threadId: ThreadId, taskId: string) => `${threadId}:${taskId}`;

// Fallback when the in-memory description cache no longer has the task name
// (server restart, session-exit sweep, TTL/capacity eviction): earlier
// task.started/task.progress activities for the task are persisted with it.
function findTaskTitleInActivities(
activities: ReadonlyArray<OrchestrationThreadActivity> | undefined,
taskId: string,
): string | undefined {
if (!activities) {
return undefined;
}
for (let index = activities.length - 1; index >= 0; index -= 1) {
const activity = activities[index];
if (!activity || (activity.kind !== "task.started" && activity.kind !== "task.progress")) {
continue;
}
const payload =
activity.payload && typeof activity.payload === "object"
? (activity.payload as { taskId?: unknown; title?: unknown; detail?: unknown })
: undefined;
if (payload?.taskId !== taskId) {
continue;
}
const title =
typeof payload.title === "string"
? payload.title
: activity.kind === "task.started" && typeof payload.detail === "string"
? payload.detail
: undefined;
if (title && title.trim().length > 0) {
return title;
}
}
return undefined;
}

interface AssistantSegmentState {
baseKey: string;
Expand All@@ -53,6 +89,8 @@ const BUFFERED_MESSAGE_TEXT_BY_MESSAGE_ID_CACHE_CAPACITY = 20_000;
const BUFFERED_MESSAGE_TEXT_BY_MESSAGE_ID_TTL = Duration.minutes(120);
const BUFFERED_PROPOSED_PLAN_BY_ID_CACHE_CAPACITY = 10_000;
const BUFFERED_PROPOSED_PLAN_BY_ID_TTL = Duration.minutes(120);
const TASK_DESCRIPTION_BY_TASK_CACHE_CAPACITY = 10_000;
const TASK_DESCRIPTION_BY_TASK_TTL = Duration.minutes(120);
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
const MAX_BUFFERED_ASSISTANT_CHARS = 24_000;
const STRICT_PROVIDER_LIFECYCLE_GUARD = process.env.T3CODE_STRICT_PROVIDER_LIFECYCLE_GUARD !== "0";

Expand DownExpand Up@@ -264,6 +302,7 @@ function requestKindFromCanonicalRequestType(

function runtimeEventToActivities(
event: ProviderRuntimeEvent,
taskTitle?: string,
): ReadonlyArray<OrchestrationThreadActivity> {
const maybeSequence = (() => {
const eventWithSequence = event as ProviderRuntimeEvent & { sessionSequence?: number };
Expand DownExpand Up@@ -473,9 +512,15 @@ function runtimeEventToActivities(
createdAt: event.createdAt,
tone: "info",
kind: "task.progress",
summary: "Reasoning update",
summary:
event.payload.description.trim().length > 0
? truncateDetail(event.payload.description, 120)
: "Reasoning update",
payload: {
taskId: event.payload.taskId,
...(event.payload.description.trim().length > 0
? { title: truncateDetail(event.payload.description, 120) }
: {}),
detail: truncateDetail(event.payload.summary ?? event.payload.description),
...(event.payload.summary ? { summary: truncateDetail(event.payload.summary) } : {}),
...(event.payload.lastToolName ? { lastToolName: event.payload.lastToolName } : {}),
Expand DownExpand Up@@ -503,7 +548,15 @@ function runtimeEventToActivities(
payload: {
taskId: event.payload.taskId,
status: event.payload.status,
...(event.payload.summary ? { detail: truncateDetail(event.payload.summary) } : {}),
...(taskTitle ? { title: truncateDetail(taskTitle, 120) } : {}),
// summary + detail mirror task.progress: clients label the row from
// summary and keep detail for the preview/expanded body.
...(event.payload.summary
? {
summary: truncateDetail(event.payload.summary),
detail: truncateDetail(event.payload.summary),
}
: {}),
...(event.payload.usage !== undefined ? { usage: event.payload.usage } : {}),
},
turnId: toTurnId(event.turnId) ?? null,
Expand DownExpand Up@@ -666,6 +719,27 @@ const make = Effect.gen(function* () {
lookup: () => Effect.succeed({ text: "", createdAt: "" }),
});

// Task names arrive on task.started/task.progress but not on task.completed,
// so remember them per task to title the completion activity.
const taskDescriptionByTaskKey = yield* Cache.make<string, string>({
capacity: TASK_DESCRIPTION_BY_TASK_CACHE_CAPACITY,
timeToLive: TASK_DESCRIPTION_BY_TASK_TTL,
lookup: () => Effect.succeed(""),
});

const rememberTaskDescription = (threadId: ThreadId, taskId: string, description: string) =>
Cache.set(taskDescriptionByTaskKey, providerTaskKey(threadId, taskId), description);

// Entries are left in place after completion so replayed or duplicate
// terminal events stay titled; TTL, capacity, and the session-exit sweep
// bound the cache.
const lookupTaskDescription = (threadId: ThreadId, taskId: string) =>
Cache.getOption(taskDescriptionByTaskKey, providerTaskKey(threadId, taskId)).pipe(
Effect.map((description) =>
Option.filter(description, (value) => value.length > 0).pipe(Option.getOrUndefined),
),
);

const resolveThreadDetail = Effect.fn("resolveThreadDetail")(function* (threadId: ThreadId) {
return yield* projectionSnapshotQuery
.getThreadDetailById(threadId)
Expand DownExpand Up@@ -1090,6 +1164,7 @@ const make = Effect.gen(function* () {
const turnKeys = Array.from(yield* Cache.keys(turnMessageIdsByTurnKey));
const assistantSegmentKeys = Array.from(yield* Cache.keys(assistantSegmentStateByTurnKey));
const proposedPlanKeys = Array.from(yield* Cache.keys(bufferedProposedPlanById));
const taskDescriptionKeys = Array.from(yield* Cache.keys(taskDescriptionByTaskKey));
yield* Effect.forEach(
turnKeys,
(key) =>
Expand DownExpand Up@@ -1125,6 +1200,12 @@ const make = Effect.gen(function* () {
: Effect.void,
{ concurrency: 1 },
).pipe(Effect.asVoid);
yield* Effect.forEach(
taskDescriptionKeys,
(key) =>
key.startsWith(prefix) ? Cache.invalidate(taskDescriptionByTaskKey, key) : Effect.void,
{ concurrency: 1 },
).pipe(Effect.asVoid);
});

const getSourceProposedPlanReferenceForPendingTurnStart = Effect.fn(
Expand DownExpand Up@@ -1654,7 +1735,22 @@ const make = Effect.gen(function* () {
}
}

const activities = runtimeEventToActivities(event);
if (event.type === "task.started" || event.type === "task.progress") {
const description = event.payload.description?.trim();
if (description) {
yield* rememberTaskDescription(thread.id, event.payload.taskId, description);
}
}
let taskTitle: string | undefined;
if (event.type === "task.completed") {
taskTitle = yield* lookupTaskDescription(thread.id, event.payload.taskId);
if (!taskTitle) {
const threadDetail = yield* getLoadedThreadDetail();
taskTitle = findTaskTitleInActivities(threadDetail?.activities, event.payload.taskId);
}
}

const activities = runtimeEventToActivities(event, taskTitle);
yield* Effect.forEach(activities, (activity) =>
providerCommandId(event, "thread-activity-append").pipe(
Effect.flatMap((commandId) =>
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); feat(server): title background-task work-log rows with the task name by t3dotgg · Pull Request #3751 · pingdotgg/t3code · GitHub
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
190 changes: 190 additions & 0 deletions apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2947,6 +2947,196 @@ describe("ProviderRuntimeIngestion", () => {
).toBe("# Plan title");
});

it("titles task activities with the task description, including on completion", async () => {
const harness = await createHarness();
const now = "2026-01-01T00:00:00.000Z";

harness.emit({
type: "task.started",
eventId: asEventId("evt-named-task-started"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-named-task"),
payload: {
taskId: "named-task-1",
description: "Typecheck mobile app",
taskType: "local_bash",
},
});

harness.emit({
type: "task.progress",
eventId: asEventId("evt-named-task-progress"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-named-task"),
payload: {
taskId: "named-task-1",
description: "Typecheck mobile app",
summary: "Running tsc across the mobile workspace.",
},
});

harness.emit({
type: "task.completed",
eventId: asEventId("evt-named-task-completed"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-named-task"),
payload: {
taskId: "named-task-1",
status: "completed",
summary: "Typecheck finished without errors.",
},
});

const thread = await waitForThread(harness.readModel, (entry) =>
entry.activities.some(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-named-task-completed",
),
);

const progress = thread.activities.find(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-named-task-progress",
);
const completed = thread.activities.find(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-named-task-completed",
);

const progressPayload =
progress?.payload && typeof progress.payload === "object"
? (progress.payload as Record<string, unknown>)
: undefined;
const completedPayload =
completed?.payload && typeof completed.payload === "object"
? (completed.payload as Record<string, unknown>)
: undefined;

expect(progress?.summary).toBe("Typecheck mobile app");
expect(progressPayload?.title).toBe("Typecheck mobile app");
expect(completed?.summary).toBe("Task completed");
expect(completedPayload?.title).toBe("Typecheck mobile app");
expect(completedPayload?.summary).toBe("Typecheck finished without errors.");
expect(completedPayload?.detail).toBe("Typecheck finished without errors.");
});

it("titles task completion from task.started when no progress event carried the name", async () => {
const harness = await createHarness();
const now = "2026-01-01T00:00:00.000Z";

harness.emit({
type: "task.started",
eventId: asEventId("evt-fast-task-started"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-fast-task"),
payload: {
taskId: "fast-task-1",
description: "wait for codex review to finish",
taskType: "local_bash",
},
});

harness.emit({
type: "task.completed",
eventId: asEventId("evt-fast-task-completed"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-fast-task"),
payload: {
taskId: "fast-task-1",
status: "completed",
},
});

const thread = await waitForThread(harness.readModel, (entry) =>
entry.activities.some(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-fast-task-completed",
),
);

const completed = thread.activities.find(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-fast-task-completed",
);
const completedPayload =
completed?.payload && typeof completed.payload === "object"
? (completed.payload as Record<string, unknown>)
: undefined;

expect(completedPayload?.title).toBe("wait for codex review to finish");
});

it("titles task completion from persisted activities after the description cache is swept", async () => {
const harness = await createHarness();
const now = "2026-01-01T00:00:00.000Z";

harness.emit({
type: "task.progress",
eventId: asEventId("evt-swept-task-progress"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-swept-task"),
payload: {
taskId: "swept-task-1",
description: "Watch round-3 CI and bots",
summary: "Polling CI checks.",
},
});

await waitForThread(harness.readModel, (entry) =>
entry.activities.some(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-swept-task-progress",
),
);

// session.exited sweeps the in-memory description cache; the completion
// that follows must recover the name from persisted activities.
harness.emit({
type: "session.exited",
eventId: asEventId("evt-swept-task-session-exited"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
payload: {},
});

harness.emit({
type: "task.completed",
eventId: asEventId("evt-swept-task-completed"),
provider: ProviderDriverKind.make("claudeAgent"),
createdAt: now,
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-swept-task"),
payload: {
taskId: "swept-task-1",
status: "completed",
summary: "CI is green.",
},
});

const thread = await waitForThread(harness.readModel, (entry) =>
entry.activities.some(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-swept-task-completed",
),
);

const completed = thread.activities.find(
(activity: ProviderRuntimeTestActivity) => activity.id === "evt-swept-task-completed",
);
const completedPayload =
completed?.payload && typeof completed.payload === "object"
? (completed.payload as Record<string, unknown>)
: undefined;

expect(completedPayload?.title).toBe("Watch round-3 CI and bots");
});

it("projects structured user input request and resolution as thread activities", async () => {
const harness = await createHarness();
const now = "2026-01-01T00:00:00.000Z";
Expand Down
102 changes: 99 additions & 3 deletions apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,42 @@ import {
import { ServerSettingsService } from "../../serverSettings.ts";

const providerTurnKey = (threadId: ThreadId, turnId: TurnId) => `${threadId}:${turnId}`;
const providerTaskKey = (threadId: ThreadId, taskId: string) => `${threadId}:${taskId}`;

// Fallback when the in-memory description cache no longer has the task name
// (server restart, session-exit sweep, TTL/capacity eviction): earlier
// task.started/task.progress activities for the task are persisted with it.
function findTaskTitleInActivities(
activities: ReadonlyArray<OrchestrationThreadActivity> | undefined,
taskId: string,
): string | undefined {
if (!activities) {
return undefined;
}
for (let index = activities.length - 1; index >= 0; index -= 1) {
const activity = activities[index];
if (!activity || (activity.kind !== "task.started" && activity.kind !== "task.progress")) {
continue;
}
const payload =
activity.payload && typeof activity.payload === "object"
? (activity.payload as { taskId?: unknown; title?: unknown; detail?: unknown })
: undefined;
if (payload?.taskId !== taskId) {
continue;
}
const title =
typeof payload.title === "string"
? payload.title
: activity.kind === "task.started" && typeof payload.detail === "string"
? payload.detail
: undefined;
if (title && title.trim().length > 0) {
return title;
}
}
return undefined;
}

interface AssistantSegmentState {
baseKey: string;
Expand All@@ -53,6 +89,8 @@ const BUFFERED_MESSAGE_TEXT_BY_MESSAGE_ID_CACHE_CAPACITY = 20_000;
const BUFFERED_MESSAGE_TEXT_BY_MESSAGE_ID_TTL = Duration.minutes(120);
const BUFFERED_PROPOSED_PLAN_BY_ID_CACHE_CAPACITY = 10_000;
const BUFFERED_PROPOSED_PLAN_BY_ID_TTL = Duration.minutes(120);
const TASK_DESCRIPTION_BY_TASK_CACHE_CAPACITY = 10_000;
const TASK_DESCRIPTION_BY_TASK_TTL = Duration.minutes(120);
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
const MAX_BUFFERED_ASSISTANT_CHARS = 24_000;
const STRICT_PROVIDER_LIFECYCLE_GUARD = process.env.T3CODE_STRICT_PROVIDER_LIFECYCLE_GUARD !== "0";

Expand DownExpand Up@@ -264,6 +302,7 @@ function requestKindFromCanonicalRequestType(

function runtimeEventToActivities(
event: ProviderRuntimeEvent,
taskTitle?: string,
): ReadonlyArray<OrchestrationThreadActivity> {
const maybeSequence = (() => {
const eventWithSequence = event as ProviderRuntimeEvent & { sessionSequence?: number };
Expand DownExpand Up@@ -473,9 +512,15 @@ function runtimeEventToActivities(
createdAt: event.createdAt,
tone: "info",
kind: "task.progress",
summary: "Reasoning update",
summary:
event.payload.description.trim().length > 0
? truncateDetail(event.payload.description, 120)
: "Reasoning update",
payload: {
taskId: event.payload.taskId,
...(event.payload.description.trim().length > 0
? { title: truncateDetail(event.payload.description, 120) }
: {}),
detail: truncateDetail(event.payload.summary ?? event.payload.description),
...(event.payload.summary ? { summary: truncateDetail(event.payload.summary) } : {}),
...(event.payload.lastToolName ? { lastToolName: event.payload.lastToolName } : {}),
Expand DownExpand Up@@ -503,7 +548,15 @@ function runtimeEventToActivities(
payload: {
taskId: event.payload.taskId,
status: event.payload.status,
...(event.payload.summary ? { detail: truncateDetail(event.payload.summary) } : {}),
...(taskTitle ? { title: truncateDetail(taskTitle, 120) } : {}),
// summary + detail mirror task.progress: clients label the row from
// summary and keep detail for the preview/expanded body.
...(event.payload.summary
? {
summary: truncateDetail(event.payload.summary),
detail: truncateDetail(event.payload.summary),
}
: {}),
...(event.payload.usage !== undefined ? { usage: event.payload.usage } : {}),
},
turnId: toTurnId(event.turnId) ?? null,
Expand DownExpand Up@@ -666,6 +719,27 @@ const make = Effect.gen(function* () {
lookup: () => Effect.succeed({ text: "", createdAt: "" }),
});

// Task names arrive on task.started/task.progress but not on task.completed,
// so remember them per task to title the completion activity.
const taskDescriptionByTaskKey = yield* Cache.make<string, string>({
capacity: TASK_DESCRIPTION_BY_TASK_CACHE_CAPACITY,
timeToLive: TASK_DESCRIPTION_BY_TASK_TTL,
lookup: () => Effect.succeed(""),
});

const rememberTaskDescription = (threadId: ThreadId, taskId: string, description: string) =>
Cache.set(taskDescriptionByTaskKey, providerTaskKey(threadId, taskId), description);

// Entries are left in place after completion so replayed or duplicate
// terminal events stay titled; TTL, capacity, and the session-exit sweep
// bound the cache.
const lookupTaskDescription = (threadId: ThreadId, taskId: string) =>
Cache.getOption(taskDescriptionByTaskKey, providerTaskKey(threadId, taskId)).pipe(
Effect.map((description) =>
Option.filter(description, (value) => value.length > 0).pipe(Option.getOrUndefined),
),
);

const resolveThreadDetail = Effect.fn("resolveThreadDetail")(function* (threadId: ThreadId) {
return yield* projectionSnapshotQuery
.getThreadDetailById(threadId)
Expand DownExpand Up@@ -1090,6 +1164,7 @@ const make = Effect.gen(function* () {
const turnKeys = Array.from(yield* Cache.keys(turnMessageIdsByTurnKey));
const assistantSegmentKeys = Array.from(yield* Cache.keys(assistantSegmentStateByTurnKey));
const proposedPlanKeys = Array.from(yield* Cache.keys(bufferedProposedPlanById));
const taskDescriptionKeys = Array.from(yield* Cache.keys(taskDescriptionByTaskKey));
yield* Effect.forEach(
turnKeys,
(key) =>
Expand DownExpand Up@@ -1125,6 +1200,12 @@ const make = Effect.gen(function* () {
: Effect.void,
{ concurrency: 1 },
).pipe(Effect.asVoid);
yield* Effect.forEach(
taskDescriptionKeys,
(key) =>
key.startsWith(prefix) ? Cache.invalidate(taskDescriptionByTaskKey, key) : Effect.void,
{ concurrency: 1 },
).pipe(Effect.asVoid);
});

const getSourceProposedPlanReferenceForPendingTurnStart = Effect.fn(
Expand DownExpand Up@@ -1654,7 +1735,22 @@ const make = Effect.gen(function* () {
}
}

const activities = runtimeEventToActivities(event);
if (event.type === "task.started" || event.type === "task.progress") {
const description = event.payload.description?.trim();
if (description) {
yield* rememberTaskDescription(thread.id, event.payload.taskId, description);
}
}
let taskTitle: string | undefined;
if (event.type === "task.completed") {
taskTitle = yield* lookupTaskDescription(thread.id, event.payload.taskId);
if (!taskTitle) {
const threadDetail = yield* getLoadedThreadDetail();
taskTitle = findTaskTitleInActivities(threadDetail?.activities, event.payload.taskId);
}
}

const activities = runtimeEventToActivities(event, taskTitle);
yield* Effect.forEach(activities, (activity) =>
providerCommandId(event, "thread-activity-append").pipe(
Effect.flatMap((commandId) =>
Expand Down
Loading