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
40 changes: 40 additions & 0 deletions docs/plans/performance-and-redundancy-hygiene-v1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Performance And Redundancy Hygiene v1 Implementation Plan

Goal: reduce Threadsmith's perceived slowness and structural redundancy without changing workflow behavior.

Scope: role packet construction, phase runner timing evidence, shared context reads, and small responsibility seams in orchestrator/fs-bridge code.

Non-goals: frontend changes, release work, global skill sync, multi-provider routing, prompt rewrites without tests, or parallel writes to committed truth.

Assumptions: reads and pure derivations can be parallelized safely; writes to committed truth, phase-run records, events, and history must remain ordered.

Verification: `npm run test --workspace @threadsmith/domain`, `npm run test --workspace @threadsmith/orchestrator`, `npm run test --workspace @threadsmith/fs-bridge`, `npm run verify:project-truth`, and `git diff --check`.

## Files

- Modify: `packages/domain/src/phaseRuns.ts`
- Modify: `packages/orchestrator/src/phaseRunner.ts`
- Modify: `packages/orchestrator/src/phaseRunner.test.ts`
- Modify: `packages/orchestrator/src/rolePackets.ts`
- Modify: `packages/orchestrator/src/phaseEvidence.ts`
- Create or modify tests near the changed orchestrator/fs-bridge seams as needed.

## Steps

1. Add a timing baseline to phase-run role runtime records so each role records packet build time, launch wait time, result apply time, result read time, and observed bridge overhead.
2. Use the timing baseline to identify duplicated reads in packet construction, especially repeated project state and latest run reads between role packet building and evidence bundle generation.
3. Introduce a shared role packet build context so independent reads can happen concurrently and repeated state/latest-run reads are reused.
4. Keep committed truth writes serial and preserve existing phase-run event ordering.
5. Clean up redundancy only where responsibility boundaries are clear; avoid generic utility modules or behavior-sensitive prompt rewrites.

## Risks

- Parallelizing writes could corrupt event order or produce confusing committed truth, so v1 only parallelizes reads and pure derivations.
- Timing fields can become noisy if they are treated as precise benchmarks; they are diagnostic hints, not acceptance criteria.
- Role packet prompt behavior is sensitive, so any prompt-adjacent cleanup needs tests or snapshots.

## Done When

- Phase-run role runtime artifacts expose enough timing detail to tell whether slowness comes from packet construction, CLI execution, result apply, or result readback.
- Tests prove the new runtime fields are recorded without changing the success path.
- A follow-up optimization slice can be selected from evidence rather than intuition.
5 changes: 5 additions & 0 deletions packages/domain/src/phaseRuns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@ export const phaseRunRoleRuntimeRecordSchema = z.object({
startedAt: z.string().min(1),
finishedAt: z.string().min(1),
durationMs: z.number().int().min(0),
packetBuildDurationMs: z.number().int().min(0).optional(),
launchWaitDurationMs: z.number().int().min(0).optional(),
resultApplyDurationMs: z.number().int().min(0).optional(),
resultReadDurationMs: z.number().int().min(0).optional(),
observedBridgeOverheadMs: z.number().int().min(0).optional(),
contextRefCount: z.number().int().min(0),
packetEstimatedChars: z.number().int().min(0),
packetEstimatedTokens: z.number().int().min(0),
Expand Down
45 changes: 38 additions & 7 deletions packages/orchestrator/src/phaseEvidence.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { execFile } from "node:child_process";
import { relative } from "node:path";
import { promisify } from "node:util";
import type {
ContextReference,
PhaseRunEvidenceBundle,
PhaseRunRecord
PhaseRunRecord,
ProjectState
} from "@threadsmith/domain";
import {
loadProjectState,
Expand Down Expand Up @@ -32,8 +34,29 @@ async function readGitSummary(projectRoot: string) {
const command = "git status --short";

try {
const { stdout } = await execFileAsync("git", ["-C", projectRoot, "status", "--short"]);
const changedFiles = parseChangedFiles(stdout);
const { stdout: topLevelStdout } = await execFileAsync("git", [
"-C",
projectRoot,
"rev-parse",
"--show-toplevel"
]);
const gitTopLevel = topLevelStdout.trim();
const projectPrefix = relative(gitTopLevel, projectRoot).replace(/\\/g, "/");
const { stdout } = await execFileAsync("git", [
"-C",
gitTopLevel,
"status",
"--short"
]);
const changedFiles = parseChangedFiles(stdout)
.filter((filePath) =>
projectPrefix === "" ||
filePath.startsWith(`${projectPrefix}/`)
)
.map((filePath) =>
projectPrefix === "" ? filePath : filePath.slice(projectPrefix.length + 1)
)
.filter(Boolean);

return {
status: changedFiles.length > 0 ? "dirty" as const : "clean" as const,
Expand Down Expand Up @@ -74,19 +97,27 @@ function staleTruthWarnings(phaseRun: PhaseRunRecord, activeOwners: string[]) {
return warnings;
}

export interface PhaseEvidenceBundleContext {
state: ProjectState;
latestRuns: Awaited<ReturnType<typeof readLatestAgentRuns>>;
}

export async function buildAndWritePhaseEvidenceBundle(
projectRoot: string,
generatedAt = new Date().toISOString()
generatedAt = new Date().toISOString(),
context?: PhaseEvidenceBundleContext
): Promise<{ bundle: PhaseRunEvidenceBundle; ref: ContextReference } | null> {
const phaseRun = await readLatestPhaseRun(projectRoot);

if (!phaseRun) {
return null;
}

const state = await loadProjectState(projectRoot);
const latestRuns = await readLatestAgentRuns(projectRoot, 4);
const git = await readGitSummary(projectRoot);
const [state, latestRuns, git] = await Promise.all([
context?.state ?? loadProjectState(projectRoot),
context?.latestRuns ?? readLatestAgentRuns(projectRoot, 4),
readGitSummary(projectRoot)
]);
const verification = decideVerificationPolicy({
phase: state.currentPhase,
acceptance: state.acceptanceState,
Expand Down
5 changes: 5 additions & 0 deletions packages/orchestrator/src/phaseRunner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,11 @@ describe("PhaseRunner", () => {
phaseRunId: phaseRun.phaseRunId,
role: "planner",
contextRefCount: expect.any(Number),
packetBuildDurationMs: expect.any(Number),
launchWaitDurationMs: expect.any(Number),
resultApplyDurationMs: expect.any(Number),
resultReadDurationMs: expect.any(Number),
observedBridgeOverheadMs: expect.any(Number),
packetEstimatedChars: expect.any(Number),
outputSummaryEstimatedChars: expect.any(Number),
verificationCommandCount: 0,
Expand Down
21 changes: 21 additions & 0 deletions packages/orchestrator/src/phaseRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ function durationMs(startedAt: string, finishedAt: string) {
return Math.max(0, finished - started);
}

function elapsedMs(startedAt: number) {
return Math.max(0, Math.round(performance.now() - startedAt));
}

function nextSuccessfulRole(
current: PhaseRunRecord,
result: ExecutionResult
Expand Down Expand Up @@ -380,17 +384,21 @@ export class PhaseRunner {
while (phaseRun.status === "running") {
const role = phaseRun.currentRole ?? "planner";
const runId = crypto.randomUUID();
const packetBuildStartedAt = performance.now();
const packet = await buildPacketForRole({
projectRoot: input.projectRoot,
role,
provider: input.provider,
runId
});
const packetBuildDurationMs = elapsedMs(packetBuildStartedAt);
await createAgentRun(input.projectRoot, packet, this.now());
const roleStartedAt = this.now();
const launchStartedAt = performance.now();
const launch = await this.roleLauncher(packet, {
startedAt: roleStartedAt
});
const launchWaitDurationMs = elapsedMs(launchStartedAt);
const launchEventPhaseRun = await appendRunEvent(input.projectRoot, phaseRun, {
title: `phase-run ${phaseRun.phaseRunId} launched ${role}`,
detail: `当前角色已启动,runId=${runId}`,
Expand All @@ -405,14 +413,18 @@ export class PhaseRunner {
});

await launch.completion;
const resultApplyStartedAt = performance.now();
if (!launch.resultAppliedByLauncher) {
await applyAgentRunResult(input.projectRoot, runId);
}
const resultApplyDurationMs = elapsedMs(resultApplyStartedAt);

const resultReadStartedAt = performance.now();
const [result, record] = await Promise.all([
readAgentRunResult(input.projectRoot, runId),
readAgentRunRecord(input.projectRoot, runId)
]);
const resultReadDurationMs = elapsedMs(resultReadStartedAt);
const latestRunRef = preferredRunArtifact(record) ?? packet.output.resultPath;
const latestSuccessfulRole = nextSuccessfulRole(phaseRun, result);
let currentSliceId = phaseRun.currentSliceId;
Expand All @@ -430,6 +442,15 @@ export class PhaseRunner {
startedAt: roleStartedAt,
finishedAt: roleFinishedAt,
durationMs: durationMs(roleStartedAt, roleFinishedAt),
packetBuildDurationMs,
launchWaitDurationMs,
resultApplyDurationMs,
resultReadDurationMs,
observedBridgeOverheadMs:
packetBuildDurationMs
+ launchWaitDurationMs
+ resultApplyDurationMs
+ resultReadDurationMs,
contextRefCount: packet.contextRefs.length,
packetEstimatedChars: estimateChars(packet),
packetEstimatedTokens: estimateTokens(estimateChars(packet)),
Expand Down
47 changes: 47 additions & 0 deletions packages/orchestrator/src/roleOrchestratorDefaults.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type {
PhaseOwner,
SkillCapability,
SkillOrchestratorConfig
} from "@threadsmith/domain";

export function builtInOnlyOrchestratorConfig(): SkillOrchestratorConfig {
return {
version: 1,
builtInProtocols: [
"brief",
"plan",
"debug",
"review",
"verify",
"closeout",
"handoff",
"recover",
"research"
],
adapters: [],
routePreferences: [],
defaultFallback: "plan",
selfHosting: {
activeController: "installed-skill",
repositorySkillPath: "codex/skills/threadsmith/SKILL.md",
installedSkillPath: "~/.codex/skills/threadsmith/SKILL.md",
allowGlobalSkillMutation: false
}
};
}

export function protocolCapabilityForRole(role: PhaseOwner): SkillCapability {
switch (role) {
case "planner":
case "executor":
return "plan";
case "reviewer":
return "review";
case "verifier":
return "verify";
case "closeout":
return "closeout";
case "hygiene":
return "recover";
}
}
53 changes: 53 additions & 0 deletions packages/orchestrator/src/rolePacketBuildContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import {
loadProjectState,
readLatestAgentRuns,
readRecentEvents
} from "@threadsmith/fs-bridge";
import { buildAndWritePhaseEvidenceBundle } from "./phaseEvidence.ts";
import { latestPhaseRunRefs } from "./rolePacketContextRefs.ts";

export interface RolePacketBuildContext {
state: Awaited<ReturnType<typeof loadProjectState>>;
recentEvents: Awaited<ReturnType<typeof readRecentEvents>>;
latestRuns: Awaited<ReturnType<typeof readLatestAgentRuns>>;
phaseRefs: Awaited<ReturnType<typeof latestPhaseRunRefs>>;
evidenceBundle: Awaited<ReturnType<typeof buildAndWritePhaseEvidenceBundle>>;
}

export async function buildRolePacketContext(
projectRoot: string
): Promise<RolePacketBuildContext> {
const statePromise = loadProjectState(projectRoot);
const latestRunsPromise = readLatestAgentRuns(projectRoot, 4);
const evidenceBundlePromise = Promise.all([
statePromise,
latestRunsPromise
]).then(([state, latestRuns]) =>
buildAndWritePhaseEvidenceBundle(projectRoot, new Date().toISOString(), {
state,
latestRuns
})
);

const [
state,
recentEvents,
latestRuns,
phaseRefs,
evidenceBundle
] = await Promise.all([
statePromise,
readRecentEvents(projectRoot, 4),
latestRunsPromise,
latestPhaseRunRefs(projectRoot),
evidenceBundlePromise
]);

return {
state,
recentEvents,
latestRuns,
phaseRefs,
evidenceBundle
};
}
Loading
Loading