Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
e1b562b
feat: native subagent & workflow observability (Agents panel, quiet t…
t3dotgg Aug 2, 2026
9be170a
feat(web): agent spawn CTA row replaces live strip and inline workflo…
t3dotgg Aug 2, 2026
16cdc33
fix(web): honest workflow status, shell exclusion, flat panel rows
t3dotgg Aug 2, 2026
4622c3a
fix(web): spawn CTA rows survive turn folding and work-group overflow
t3dotgg Aug 2, 2026
3ee8054
fix(web): all in-flight subagent states present as Working
t3dotgg Aug 2, 2026
4947acc
fix(web): show spawn CTA during the run, anchored at the spawn point
t3dotgg Aug 2, 2026
8ebdb72
feat(web): sidebar liveness beyond the turn — Working and Monitoring
t3dotgg Aug 2, 2026
3573c9c
fix(web): standard subagents join the roster — task-type denylist
t3dotgg Aug 2, 2026
95c2047
fix: Codex collab children reach the CTA and panel with real names
t3dotgg Aug 2, 2026
0984bab
fix: idle Codex agents present as settled; names survive progress rows
t3dotgg Aug 2, 2026
23f75ed
fix(server): never intercept the Codex root thread as its own child
t3dotgg Aug 2, 2026
eb0fd9c
fix(web): one CTA per Claude parallel batch — pin membership at first…
t3dotgg Aug 2, 2026
a779285
fix(server): keep subagent-owned Claude traffic out of the parent tra…
t3dotgg Aug 2, 2026
9262161
fix: subagent-internal shells attributed to their owning agent
t3dotgg Aug 2, 2026
b16ee95
feat(web): workflow phase rail + read-only script access
t3dotgg Aug 2, 2026
a266b19
feat: stop-everything interrupt + collapsible phase sections
t3dotgg Aug 2, 2026
8d20b68
fix: CI + review round — lint, service conventions, TOCTOU, stop binding
t3dotgg Aug 2, 2026
239a013
fix: effect-diagnostics conformance for CI Check
t3dotgg Aug 2, 2026
77fa3f4
refactor(server): canonical ThreadBackgroundLiveness module, visible …
t3dotgg Aug 2, 2026
3585e63
refactor(server): canonical make/layer exports + bucket reclassificat…
t3dotgg Aug 2, 2026
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
46 changes: 46 additions & 0 deletions apps/mobile/src/lib/threadActivity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ interface WorkLogEntry {
interface DerivedWorkLogEntry extends WorkLogEntry {
activityKind: OrchestrationThreadActivity["kind"];
collapseKey?: string;
/** Grouping key for subagent lifecycle rows (one row per agent). */
taskId?: string;
}

type RawThreadFeedEntry =
Expand Down Expand Up @@ -235,6 +237,26 @@ function resolvePendingUserInputAnswer(
return normalizeDraftAnswer(draft?.selectedOptionLabel);
}

/**
* Quiet-timeline guarantee (mirrors web's session-logic): agent-internal
* activity lives in the Agents sheet, not the work log. task.completed rows
* are kept — with no workflow card on mobile they are the terminal signal
* (a surface that hides rows must keep its own terminal signal).
*/
function isAgentInternalActivity(activity: OrchestrationThreadActivity): boolean {
const payload =
activity.payload && typeof activity.payload === "object"
? (activity.payload as Record<string, unknown>)
: null;
if (!payload) {
return false;
}
if (payload.timelineBypass === true && activity.kind !== "task.completed") {
return true;
}
return typeof payload.agentId === "string" && payload.agentId.trim().length > 0;
}

function deriveWorkLogEntries(
activities: ReadonlyArray<OrchestrationThreadActivity>,
): DerivedWorkLogEntry[] {
Expand All @@ -243,9 +265,12 @@ function deriveWorkLogEntries(
for (const activity of ordered) {
if (activity.kind === "tool.started") continue;
if (activity.kind === "task.started") continue;
if (activity.kind === "task.updated") continue;
if (activity.kind === "tool.progress") continue;
if (activity.kind === "context-window.updated") continue;
if (activity.summary === "Checkpoint captured") continue;
if (isPlanBoundaryToolActivity(activity)) continue;
if (isAgentInternalActivity(activity)) continue;
entries.push(toDerivedWorkLogEntry(activity));
}
return collapseDerivedWorkLogEntries(entries);
Expand Down Expand Up @@ -284,10 +309,15 @@ function toDerivedWorkLogEntry(activity: OrchestrationThreadActivity): DerivedWo
? payload.detail
: null;
const taskLabel = taskSummary || taskDetailAsLabel;
const taskId =
isTaskActivity && typeof payload?.taskId === "string" && payload.taskId.length > 0
? payload.taskId
: undefined;
const entry: DerivedWorkLogEntry = {
id: activity.id,
createdAt: activity.createdAt,
turnId: activity.turnId,
...(taskId ? { taskId } : {}),
label: taskLabel || activity.summary,
tone:
activity.kind === "task.progress"
Expand Down Expand Up @@ -352,7 +382,23 @@ function collapseDerivedWorkLogEntries(
entries: ReadonlyArray<DerivedWorkLogEntry>,
): DerivedWorkLogEntry[] {
const collapsed: DerivedWorkLogEntry[] = [];
// Subagent rows collapse by identity, not adjacency (quiet-timeline
// guarantee; mirrors web's session-logic).
const taskRowIndex = new Map<string, number>();
for (const entry of entries) {
const isTaskRow =
entry.taskId !== undefined &&
(entry.activityKind === "task.progress" || entry.activityKind === "task.completed");
if (isTaskRow && entry.taskId !== undefined) {
const existingIndex = taskRowIndex.get(entry.taskId);
if (existingIndex !== undefined) {
collapsed[existingIndex] = mergeDerivedWorkLogEntries(collapsed[existingIndex]!, entry);
continue;
}
taskRowIndex.set(entry.taskId, collapsed.length);
collapsed.push(entry);
continue;
}
const previous = collapsed.at(-1);
if (previous && shouldCollapseToolLifecycleEntries(previous, entry)) {
collapsed[collapsed.length - 1] = mergeDerivedWorkLogEntries(previous, entry);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import * as RepositoryIdentityResolver from "../src/project/RepositoryIdentityRe
import { OrchestrationEngineLive } from "../src/orchestration/Layers/OrchestrationEngine.ts";
import { OrchestrationProjectionPipelineLive } from "../src/orchestration/Layers/ProjectionPipeline.ts";
import { OrchestrationProjectionSnapshotQueryLive } from "../src/orchestration/Layers/ProjectionSnapshotQuery.ts";
import * as ThreadBackgroundLiveness from "../src/orchestration/ThreadBackgroundLiveness.ts";
import { RuntimeReceiptBusTest } from "../src/orchestration/Layers/RuntimeReceiptBus.ts";
import { OrchestrationReactorLive } from "../src/orchestration/Layers/OrchestrationReactor.ts";
import { ProviderCommandReactorLive } from "../src/orchestration/Layers/ProviderCommandReactor.ts";
Expand Down Expand Up @@ -305,7 +306,7 @@ export const makeOrchestrationIntegrationHarness = (
checkpointStoreLayer,
providerLayer,
RuntimeReceiptBusTest,
);
).pipe(Layer.provideMerge(ThreadBackgroundLiveness.layer));
const serverSettingsLayer = ServerSettingsService.layerTest();
const runtimeIngestionLayer = ProviderRuntimeIngestionLive.pipe(
Layer.provideMerge(runtimeServicesLayer),
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/auth/RpcAuthorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type WsRpcMethod = RpcGroup.Rpcs<typeof WsRpcGroup>["_tag"];
*/
export const RPC_REQUIRED_SCOPES = {
[ORCHESTRATION_WS_METHODS.dispatchCommand]: AuthOrchestrationOperateScope,
[ORCHESTRATION_WS_METHODS.getWorkflowScript]: AuthOrchestrationReadScope,
[ORCHESTRATION_WS_METHODS.getTurnDiff]: AuthOrchestrationReadScope,
[ORCHESTRATION_WS_METHODS.getFullThreadDiff]: AuthOrchestrationReadScope,
[ORCHESTRATION_WS_METHODS.searchThreads]: AuthOrchestrationReadScope,
Expand Down
62 changes: 62 additions & 0 deletions apps/server/src/orchestration/ActivityPayloadProjection.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { describe, expect, it } from "vite-plus/test";
import type { OrchestrationThreadActivity } from "@t3tools/contracts";
import { projectActivityPayload } from "./ActivityPayloadProjection.ts";

function activity(payload: Record<string, unknown>): OrchestrationThreadActivity {
return {
id: "activity-1",
tone: "tool",
kind: "tool.completed",
summary: "Tool",
payload,
turnId: null,
createdAt: "2026-08-01T10:00:00.000Z",
} as unknown as OrchestrationThreadActivity;
}

/**
* Wire-survival regression: the slimming pass rewrites payload.data but must
* never strip the top-level per-agent fields the subagent fold depends on.
* If slimming ever moves to an allowlist over the whole payload, these
* assertions are the tripwire.
*/
describe("projectActivityPayload agent-field survival", () => {
it("preserves tool attribution (agentId/parentToolUseId) through data slimming", () => {
const projected = projectActivityPayload(
activity({
itemType: "command_execution",
agentId: "task-123",
parentToolUseId: "toolu_abc",
data: {
toolName: "Bash",
input: { command: "ls" },
command: "ls",
rawOutput: { content: "x".repeat(10) },
somethingClientNeverReads: { big: "blob" },
},
}),
);
const payload = projected.payload as Record<string, unknown>;
expect(payload.agentId).toBe("task-123");
expect(payload.parentToolUseId).toBe("toolu_abc");
// Slimming itself still applies to data.
const data = payload.data as Record<string, unknown>;
expect(data.somethingClientNeverReads).toBeUndefined();
});

it("passes task lifecycle payloads (no data field) through untouched", () => {
const source = activity({
taskId: "task-9",
title: "Audit auth",
role: "explorer",
model: "opus",
workflowName: "audit-flow",
phases: [{ index: 0, title: "Audit" }],
typedUsage: { totalTokens: 1200 },
runHandles: { runId: "run-1", scriptPath: "/tmp/wf.js" },
timelineBypass: true,
});
const projected = projectActivityPayload(source);
expect(projected.payload).toEqual(source.payload);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { CheckpointReactorLive } from "./CheckpointReactor.ts";
import { OrchestrationEngineLive } from "./OrchestrationEngine.ts";
import { OrchestrationProjectionPipelineLive } from "./ProjectionPipeline.ts";
import { OrchestrationProjectionSnapshotQueryLive } from "./ProjectionSnapshotQuery.ts";
import * as ThreadBackgroundLiveness from "../ThreadBackgroundLiveness.ts";
import { RuntimeReceiptBusLive } from "./RuntimeReceiptBus.ts";
import { OrchestrationEventStoreLive } from "../../persistence/Layers/OrchestrationEventStore.ts";
import { OrchestrationCommandReceiptRepositoryLive } from "../../persistence/Layers/OrchestrationCommandReceipts.ts";
Expand Down Expand Up @@ -294,13 +295,15 @@ describe("CheckpointReactor", () => {
);
const orchestrationLayer = OrchestrationEngineLive.pipe(
Layer.provide(OrchestrationProjectionSnapshotQueryLive),
Layer.provide(ThreadBackgroundLiveness.layer),
Layer.provide(OrchestrationProjectionPipelineLive),
Layer.provide(OrchestrationEventStoreLive),
Layer.provide(OrchestrationCommandReceiptRepositoryLive),
Layer.provide(RepositoryIdentityResolver.layer),
Layer.provide(SqlitePersistenceMemory),
);
const projectionSnapshotLayer = OrchestrationProjectionSnapshotQueryLive.pipe(
Layer.provide(ThreadBackgroundLiveness.layer),
Layer.provide(RepositoryIdentityResolver.layer),
Layer.provide(SqlitePersistenceMemory),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import * as RepositoryIdentityResolver from "../../project/RepositoryIdentityRes
import { OrchestrationEngineLive } from "./OrchestrationEngine.ts";
import { OrchestrationProjectionPipelineLive } from "./ProjectionPipeline.ts";
import { OrchestrationProjectionSnapshotQueryLive } from "./ProjectionSnapshotQuery.ts";
import * as ThreadBackgroundLiveness from "../ThreadBackgroundLiveness.ts";
import { OrchestrationEngineService } from "../Services/OrchestrationEngine.ts";
import {
OrchestrationProjectionPipeline,
Expand All @@ -55,6 +56,7 @@ async function createOrchestrationSystem() {
),
OrchestrationProjectionSnapshotQueryLive,
).pipe(
Layer.provide(ThreadBackgroundLiveness.layer),
Layer.provide(OrchestrationEventStoreLive),
Layer.provide(OrchestrationCommandReceiptRepositoryLive),
Layer.provide(RepositoryIdentityResolver.layer),
Expand Down Expand Up @@ -817,6 +819,7 @@ describe("OrchestrationEngine", () => {
const runtime = ManagedRuntime.make(
OrchestrationEngineLive.pipe(
Layer.provide(OrchestrationProjectionSnapshotQueryLive),
Layer.provide(ThreadBackgroundLiveness.layer),
Layer.provide(OrchestrationProjectionPipelineLive),
Layer.provide(Layer.succeed(OrchestrationEventStore, flakyStore)),
Layer.provide(OrchestrationCommandReceiptRepositoryLive),
Expand Down Expand Up @@ -922,6 +925,7 @@ describe("OrchestrationEngine", () => {
const runtime = ManagedRuntime.make(
OrchestrationEngineLive.pipe(
Layer.provide(OrchestrationProjectionSnapshotQueryLive),
Layer.provide(ThreadBackgroundLiveness.layer),
Layer.provide(Layer.succeed(OrchestrationProjectionPipeline, flakyProjectionPipeline)),
Layer.provide(OrchestrationEventStoreLive),
Layer.provide(OrchestrationCommandReceiptRepositoryLive),
Expand Down Expand Up @@ -1065,6 +1069,7 @@ describe("OrchestrationEngine", () => {
const runtime = ManagedRuntime.make(
OrchestrationEngineLive.pipe(
Layer.provide(OrchestrationProjectionSnapshotQueryLive),
Layer.provide(ThreadBackgroundLiveness.layer),
Layer.provide(Layer.succeed(OrchestrationProjectionPipeline, flakyProjectionPipeline)),
Layer.provide(Layer.succeed(OrchestrationEventStore, nonTransactionalStore)),
Layer.provide(OrchestrationCommandReceiptRepositoryLive),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
OrchestrationProjectionPipelineLive,
} from "./ProjectionPipeline.ts";
import { OrchestrationProjectionSnapshotQueryLive } from "./ProjectionSnapshotQuery.ts";
import * as ThreadBackgroundLiveness from "../ThreadBackgroundLiveness.ts";
import { OrchestrationEngineService } from "../Services/OrchestrationEngine.ts";
import { OrchestrationProjectionPipeline } from "../Services/ProjectionPipeline.ts";
import { ServerConfig } from "../../config.ts";
Expand Down Expand Up @@ -2664,6 +2665,7 @@ it.effect("restores pending turn-start metadata across projection pipeline resta
const engineLayer = it.layer(
OrchestrationEngineLive.pipe(
Layer.provide(OrchestrationProjectionSnapshotQueryLive),
Layer.provide(ThreadBackgroundLiveness.layer),
Layer.provide(OrchestrationProjectionPipelineLive),
Layer.provide(OrchestrationEventStoreLive),
Layer.provide(OrchestrationCommandReceiptRepositoryLive),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { SqlitePersistenceMemory } from "../../persistence/Layers/Sqlite.ts";
import * as RepositoryIdentityResolver from "../../project/RepositoryIdentityResolver.ts";
import { ORCHESTRATION_PROJECTOR_NAMES } from "./ProjectionPipeline.ts";
import { OrchestrationProjectionSnapshotQueryLive } from "./ProjectionSnapshotQuery.ts";
import * as ThreadBackgroundLiveness from "../ThreadBackgroundLiveness.ts";
import { ProjectionSnapshotQuery } from "../Services/ProjectionSnapshotQuery.ts";

const asProjectId = (value: string): ProjectId => ProjectId.make(value);
Expand All @@ -27,6 +28,7 @@ const asCheckpointRef = (value: string): CheckpointRef => CheckpointRef.make(val

const projectionSnapshotLayer = it.layer(
OrchestrationProjectionSnapshotQueryLive.pipe(
Layer.provide(ThreadBackgroundLiveness.layer),
Layer.provideMerge(RepositoryIdentityResolver.layer),
Layer.provideMerge(SqlitePersistenceMemory),
Layer.provideMerge(NodeServices.layer),
Expand Down Expand Up @@ -441,6 +443,7 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => {
hasPendingApprovals: true,
hasPendingUserInput: false,
hasActionableProposedPlan: false,
backgroundLiveness: null,
},
]);

Expand Down Expand Up @@ -1823,6 +1826,7 @@ it.effect(
() => {
const resolveCalls: string[] = [];
const layer = OrchestrationProjectionSnapshotQueryLive.pipe(
Layer.provide(ThreadBackgroundLiveness.layer),
Layer.provideMerge(
Layer.succeed(RepositoryIdentityResolver.RepositoryIdentityResolver, {
resolve: (cwd: string) =>
Expand Down
11 changes: 11 additions & 0 deletions apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {
type ProjectionRepositoryError,
} from "../../persistence/Errors.ts";
import { ProjectionCheckpoint } from "../../persistence/Services/ProjectionCheckpoints.ts";
import { ThreadBackgroundLivenessService } from "../ThreadBackgroundLiveness.ts";
import { ProjectionProject } from "../../persistence/Services/ProjectionProjects.ts";
import { ProjectionState } from "../../persistence/Services/ProjectionState.ts";
import { ProjectionThreadActivity } from "../../persistence/Services/ProjectionThreadActivities.ts";
Expand Down Expand Up @@ -308,6 +309,7 @@ function toPersistenceSqlOrDecodeError(sqlOperation: string, decodeOperation: st
}

const makeProjectionSnapshotQuery = Effect.gen(function* () {
const threadBackgroundLiveness = yield* ThreadBackgroundLivenessService;
const sql = yield* SqlClient.SqlClient;
const repositoryIdentityResolver = yield* RepositoryIdentityResolver.RepositoryIdentityResolver;
const repositoryIdentityResolutionConcurrency = 4;
Expand Down Expand Up @@ -1671,6 +1673,9 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
hasPendingApprovals: row.pendingApprovalCount > 0,
hasPendingUserInput: row.pendingUserInputCount > 0,
hasActionableProposedPlan: row.hasActionableProposedPlan > 0,
backgroundLiveness: threadBackgroundLiveness.getThreadBackgroundLiveness(
row.threadId,
),
} satisfies OrchestrationThreadShell)
: Result.failVoid,
),
Expand Down Expand Up @@ -1810,6 +1815,9 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
hasPendingApprovals: row.pendingApprovalCount > 0,
hasPendingUserInput: row.pendingUserInputCount > 0,
hasActionableProposedPlan: row.hasActionableProposedPlan > 0,
backgroundLiveness: threadBackgroundLiveness.getThreadBackgroundLiveness(
row.threadId,
),
}),
),
updatedAt: updatedAt ?? "1970-01-01T00:00:00.000Z",
Expand Down Expand Up @@ -2081,6 +2089,9 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
hasPendingApprovals: threadRow.value.pendingApprovalCount > 0,
hasPendingUserInput: threadRow.value.pendingUserInputCount > 0,
hasActionableProposedPlan: threadRow.value.hasActionableProposedPlan > 0,
backgroundLiveness: threadBackgroundLiveness.getThreadBackgroundLiveness(
threadRow.value.threadId,
),
} satisfies OrchestrationThreadShell);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import * as RepositoryIdentityResolver from "../../project/RepositoryIdentityRes
import { OrchestrationEngineLive } from "./OrchestrationEngine.ts";
import { OrchestrationProjectionPipelineLive } from "./ProjectionPipeline.ts";
import { OrchestrationProjectionSnapshotQueryLive } from "./ProjectionSnapshotQuery.ts";
import * as ThreadBackgroundLiveness from "../ThreadBackgroundLiveness.ts";
import {
providerErrorLabel,
providerErrorLabelFromInstanceHint,
Expand Down Expand Up @@ -345,13 +346,15 @@ describe("ProviderCommandReactor", () => {

const orchestrationLayer = OrchestrationEngineLive.pipe(
Layer.provide(OrchestrationProjectionSnapshotQueryLive),
Layer.provide(ThreadBackgroundLiveness.layer),
Layer.provide(OrchestrationProjectionPipelineLive),
Layer.provide(OrchestrationEventStoreLive),
Layer.provide(OrchestrationCommandReceiptRepositoryLive),
Layer.provide(RepositoryIdentityResolver.layer),
Layer.provide(SqlitePersistenceMemory),
);
const projectionSnapshotLayer = OrchestrationProjectionSnapshotQueryLive.pipe(
Layer.provide(ThreadBackgroundLiveness.layer),
Layer.provide(RepositoryIdentityResolver.layer),
Layer.provide(SqlitePersistenceMemory),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import * as RepositoryIdentityResolver from "../../project/RepositoryIdentityRes
import { OrchestrationEngineLive } from "./OrchestrationEngine.ts";
import { OrchestrationProjectionPipelineLive } from "./ProjectionPipeline.ts";
import { OrchestrationProjectionSnapshotQueryLive } from "./ProjectionSnapshotQuery.ts";
import * as ThreadBackgroundLiveness from "../ThreadBackgroundLiveness.ts";
import { ProviderRuntimeIngestionLive } from "./ProviderRuntimeIngestion.ts";
import { OrchestrationEngineService } from "../Services/OrchestrationEngine.ts";
import { ProviderRuntimeIngestionService } from "../Services/ProviderRuntimeIngestion.ts";
Expand Down Expand Up @@ -237,6 +238,9 @@ describe("ProviderRuntimeIngestion", () => {
const layer = ProviderRuntimeIngestionLive.pipe(
Layer.provideMerge(orchestrationLayer),
Layer.provideMerge(projectionSnapshotLayer),
// Single shared liveness instance across ingestion (writer), the
// engine, and the snapshot query (reader).
Layer.provideMerge(ThreadBackgroundLiveness.layer),
Layer.provideMerge(SqlitePersistenceMemory),
Layer.provideMerge(Layer.succeed(ProviderService, provider.service)),
Layer.provideMerge(makeTestServerSettingsLayer(options?.serverSettings)),
Expand Down
Loading
Loading