FN-8444: include planning time in task metrics
Track active planning time alongside execution time for costs, analytics, and task displays. - Persist planning timing state across task lifecycle transitions and recovery - Include planning activity in token cost, analytics, and dashboard timing displays - Add PostgreSQL migration support using the configured migration directory Files changed: .changeset/fn-8444-planning-time-cost.md | 7 +++ docs/dashboard-guide.md | 3 ++ docs/task-management.md | 5 ++ packages/core/src/index.ts | 1 + .../migrations/0029_planning_active_timing.sql | 3 ++ packages/core/src/postgres/schema-applier.ts | 14 ++++- packages/core/src/postgres/schema/project.ts | 2 + packages/core/src/productivity-analytics.ts | 29 +++++----- packages/core/src/store.ts | 2 +- .../core/src/task-store/archive-lifecycle-2.ts | 2 + packages/core/src/task-store/moves.ts | 7 +++ packages/core/src/task-store/persistence.ts | 4 ++ packages/core/src/task-store/remaining-ops-2.ts | 2 +- packages/core/src/task-store/serialization.ts | 7 +++ packages/core/src/task-store/task-row-mappers.ts | 2 +- packages/core/src/task-store/task-update.ts | 10 ++++ packages/core/src/task-timing.ts | 35 ++++++++++++ packages/core/src/types.ts | 12 +++++ packages/dashboard/app/components/TaskCard.tsx | 13 ++--- .../app/components/TaskTokenStatsPanel.tsx | 6 ++- .../app/components/__tests__/TaskCard.test.tsx | 17 ++++++ .../app/utils/__tests__/taskTiming.test.ts | 9 +++- packages/dashboard/app/utils/taskTiming.ts | 14 +++++ packages/dashboard/app/utils/taskTokenCost.ts | 2 + .../dashboard/src/task-planner-chat-metrics.ts | 14 ++++- packages/engine/src/__tests__/self-healing.test.ts | 61 +++++++++++++++++++++ packages/engine/src/executor.ts | 50 +++++++++++++++++ packages/engine/src/runtimes/in-process-runtime.ts | 3 ++ packages/engine/src/self-healing.ts | 62 ++++++++++++++++++++++ packages/engine/src/triage.ts | 10 ++++ packages/i18n/locales/en/app.json | 2 +- packages/i18n/locales/es/app.json | 2 +- packages/i18n/locales/fr/app.json | 2 +- packages/i18n/locales/ko/app.json | 2 +- packages/i18n/locales/zh-CN/app.json | 2 +- packages/i18n/locales/zh-TW/app.json | 2 +- 36 files changed, 384 insertions(+), 36 deletions(-) Fusion-Task-Id: FN-8444 Fusion-Task-Lineage: 0178e0a7-3018-4ef4-be9b-6de5f964fb58 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
@@ -96,6 +96,7 @@ import { Type, type Static } from "@earendil-works/pi-ai";
|
||||
import { describeModel, formatModelMarkerDetails, promptWithFallback, compactSessionContext } from "./pi.js";
|
||||
import { buildAgentGatedActionSummary } from "./permanent-agent-gating.js";
|
||||
import { accumulateSessionTokenUsage, captureSessionTokenBaseline, mergeTokenUsagePerModel, resetSessionTokenBaseline } from "./session-token-usage.js";
|
||||
import { finalizePlanningSegment, startPlanningSegment } from "@fusion/core";
|
||||
import { enforceTaskTokenBudgetForPersist } from "./token-budget-enforcer.js";
|
||||
import {
|
||||
createResolvedAgentSession,
|
||||
@@ -1730,6 +1731,12 @@ export class TaskExecutor {
|
||||
private effectiveColumnAgentByTask = new Map<string, string>();
|
||||
/** Active pre-merge workflow step sessions per task. */
|
||||
private activeWorkflowStepSessions = new Map<string, AgentSession>();
|
||||
/**
|
||||
* FNXC:TaskTiming 2026-08-01-12:00:
|
||||
* Only graph-owned Plan Review sessions appear here. Self-healing uses this
|
||||
* narrow liveness proof so it never finalizes an in-flight planning segment.
|
||||
*/
|
||||
private activePlanningWorkflowSessions = new Set<string>();
|
||||
/** Steering comments already observed for active workflow step sessions. */
|
||||
private activeWorkflowStepSessionSeenSteeringIds = new Map<string, Set<string>>();
|
||||
/** Active configured-command abort controllers keyed by task. */
|
||||
@@ -2459,6 +2466,17 @@ export class TaskExecutor {
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* FNXC:TaskTiming 2026-08-01-12:00:
|
||||
* A planning segment has one owner: a graph Plan Review session is live only
|
||||
* while both its session registration and planning ownership marker remain.
|
||||
* This is intentionally narrower than isTaskActive(), which also covers
|
||||
* implementation and non-planning workflow sessions.
|
||||
*/
|
||||
hasActivePlanningWorkflowSession(taskId: string): boolean {
|
||||
return this.activePlanningWorkflowSessions.has(taskId) && this.activeWorkflowStepSessions.has(taskId);
|
||||
}
|
||||
|
||||
isTaskActive(taskId: string): boolean {
|
||||
return (
|
||||
this.executing.has(taskId)
|
||||
@@ -16635,6 +16653,19 @@ You have access to the file system to review changes.${inlineFixBlock}${verdictB
|
||||
`Workflow step '${workflowStep.name}' using model: ${workflowModelDetails}`,
|
||||
);
|
||||
this.setActiveWorkflowStepSession(task.id, session, worktreePath, this.createSeenSteeringIds(task));
|
||||
// FNXC:TaskTiming 2026-08-01-10:00: graph-owned Plan Review is the only
|
||||
// post-spec planning lane. Start before prompting and finalize in finally before any replan handoff.
|
||||
const ownsPlanningSegment = workflowStep.id === "graph:plan-review-step" || workflowStep.name === "Plan Review";
|
||||
if (ownsPlanningSegment) {
|
||||
this.activePlanningWorkflowSessions.add(task.id);
|
||||
const planningStart = startPlanningSegment(task);
|
||||
try {
|
||||
if (planningStart.planningStartedAt) await this.store.updateTask(task.id, planningStart);
|
||||
} catch (error) {
|
||||
this.activePlanningWorkflowSessions.delete(task.id);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
let output = "";
|
||||
const deltaNormalizer = createStreamingDeltaNormalizer();
|
||||
@@ -16717,6 +16748,9 @@ You have access to the file system to review changes.${inlineFixBlock}${verdictB
|
||||
if (workflowStep.requiresBrowser === true) {
|
||||
await logBrowserVerificationActivity(`[browser-verification] finished browser verification for task ${task.id}: timed out`);
|
||||
}
|
||||
// FNXC:TaskCost 2026-08-01-10:00: Plan Review tokens are task cost;
|
||||
// snapshot before timeout disposal just like normal completion.
|
||||
await accumulateSessionTokenUsage(this.store, task.id, session, { agentId: task.assignedAgentId ?? undefined, role: "executor" });
|
||||
try { session.dispose(); } catch { /* best-effort */ }
|
||||
await agentLogger.flush();
|
||||
return { success: false, error: `workflow step timed out after ${timeoutMs}ms`, timedOut: true };
|
||||
@@ -16772,6 +16806,9 @@ You have access to the file system to review changes.${inlineFixBlock}${verdictB
|
||||
return { success: true, output: parsed.output };
|
||||
} catch (err: unknown) {
|
||||
await agentLogger.flush();
|
||||
// Persist the delta before error disposal so graph-owned planning reviews
|
||||
// cannot disappear from operator cost totals.
|
||||
await accumulateSessionTokenUsage(this.store, task.id, session, { agentId: task.assignedAgentId ?? undefined, role: "executor" });
|
||||
try { session.dispose(); } catch { /* best-effort */ }
|
||||
if ((err instanceof ReadonlyViolationError) || ((err as { code?: string } | null)?.code === "READONLY_VIOLATION")) {
|
||||
const violation = err as ReadonlyViolationError;
|
||||
@@ -16792,6 +16829,19 @@ You have access to the file system to review changes.${inlineFixBlock}${verdictB
|
||||
return { success: false, error: errorMessage };
|
||||
} finally {
|
||||
if (timeoutHandle) clearTimeout(timeoutHandle);
|
||||
if (ownsPlanningSegment) {
|
||||
try {
|
||||
const livePlanningTask = await this.store.getTask(task.id);
|
||||
if (livePlanningTask) {
|
||||
const planningEnd = finalizePlanningSegment(livePlanningTask);
|
||||
if (planningEnd.planningStartedAt === null) await this.store.updateTask(task.id, planningEnd);
|
||||
}
|
||||
} finally {
|
||||
// Finalize before releasing Plan Review ownership so triage can only
|
||||
// begin a subsequent, non-overlapping planning segment.
|
||||
this.activePlanningWorkflowSessions.delete(task.id);
|
||||
}
|
||||
}
|
||||
const activeWorkflowStepSession = this.activeWorkflowStepSessions.get(task.id);
|
||||
if (activeWorkflowStepSession === session) {
|
||||
this.deleteActiveWorkflowStepSession(task.id, worktreePath);
|
||||
|
||||
Reference in New Issue
Block a user