Skip to content

Commit ebaa0e7

Browse files
Merge remote-tracking branch 'origin/main' into codex/fix-23505-omp-elicitation
# Conflicts: # apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts # packages/provider-acp-registry/src/server/adapter.ts
2 parents 122d1e0 + 44ee2b9 commit ebaa0e7

86 files changed

Lines changed: 4879 additions & 1370 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/ci.yml‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -504,8 +504,9 @@ jobs:
504504
T3CODE_TRANSFER_BUDGET_REPORT_PATH: ${{ runner.temp }}/t3code-transfer-budget.md
505505
T3CODE_TRANSFER_BUDGET_RESULT_PATH: ${{ runner.temp }}/thread-transfer-result.json
506506
T3CODE_TRANSFER_BUDGET_SCENARIOS_PATH: ${{ runner.temp }}/thread-transfer-scenarios.json
507+
# One process per file spends ~2s starting before its tests run; one file per vCPU overlaps that.
507508
run: >-
508-
vp run test:cached -- --shard ${{ matrix.shard }}/${{ strategy.job-total }}
509+
vp run test:cached -- --shard ${{ matrix.shard }}/${{ strategy.job-total }} --concurrency 4
509510
--uncached apps/server/integration/transferBudgetV2.integration.test.ts
510511
--verify-hits 1 apps/server
511512

‎apps/desktop/src/updates/DesktopUpdates.test.ts‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,26 @@ describe("DesktopUpdates", () => {
247247
).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer)));
248248
});
249249

250+
it.effect("checks shortly after startup and then every four hours", () => {
251+
const harness = makeHarness();
252+
253+
return Effect.scoped(
254+
Effect.gen(function* () {
255+
const updates = yield* DesktopUpdates.DesktopUpdates;
256+
yield* updates.configure;
257+
258+
yield* TestClock.adjust("15 seconds");
259+
assert.equal(harness.checkCount(), 1);
260+
261+
yield* TestClock.adjust("3 hours");
262+
assert.equal(harness.checkCount(), 1);
263+
264+
yield* TestClock.adjust("1 hour");
265+
assert.equal(harness.checkCount(), 2);
266+
}),
267+
).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer)));
268+
});
269+
250270
it.effect(
251271
"rejects install while a refresh check is in progress and releases the reservation",
252272
() =>

‎apps/desktop/src/updates/DesktopUpdates.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ import {
6060
const CHANNEL_CHANGE_RETRY_DELAY = Duration.seconds(1);
6161
const CHANNEL_CHANGE_RETRIES = 30;
6262
const AUTO_UPDATE_STARTUP_DELAY = "15 seconds";
63-
const AUTO_UPDATE_POLL_INTERVAL = "4 minutes";
63+
const AUTO_UPDATE_POLL_INTERVAL = "4 hours";
6464
const PREPARED_INSTALL_CHECK_WAIT = Duration.seconds(90);
6565

6666
type UpdateAction = "check" | "download" | "install" | "install-recovery" | "channel";

‎apps/mobile/src/features/threads/ThreadRouteScreen.tsx‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1071,9 +1071,7 @@ function ThreadRouteContent(
10711071
onRemoveDraftImage={composer.onRemoveDraftImage}
10721072
serverConfig={serverConfig}
10731073
onStopThread={awaitingBootstrapTurn ? handleCancelWorktreeSetup : handleStopThread}
1074-
onResumeQueue={
1075-
composer.queueHeld || composer.resumableRunId !== null ? handleResumeQueue : null
1076-
}
1074+
onResumeQueue={composer.canResume ? handleResumeQueue : null}
10771075
onSendMessage={composer.onSendMessage}
10781076
offersSideChat={composer.sideChatUnavailableReason === null}
10791077
onReconnectEnvironment={handleReconnectEnvironment}

‎apps/mobile/src/state/use-thread-composer-state.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
isDetailBehindShell,
1414
} from "@t3tools/client-runtime/state/thread-activity-announcement";
1515
import {
16+
canResumeThread,
1617
deriveProviderSubagentStatus,
1718
deriveProviderExtensionStatuses,
1819
deriveResumableRunId,
@@ -401,6 +402,9 @@ export function useThreadComposerState(input: {
401402
const resumableRunId = selectedThreadProjection
402403
? deriveResumableRunId(selectedThreadProjection.projection, selectedThreadRuntime)
403404
: null;
405+
const canResume = selectedThreadProjection
406+
? canResumeThread(selectedThreadProjection.projection, resumableRunId, queueHeld)
407+
: queueHeld;
404408

405409
const runlessWorkStartedAt = selectedThreadProjection
406410
? deriveRunlessWorkStartedAt(selectedThreadProjection.projection)
@@ -919,6 +923,7 @@ export function useThreadComposerState(input: {
919923
canSteerActiveTurn,
920924
queueHeld,
921925
resumableRunId,
926+
canResume,
922927
queuedRunEdit,
923928
isSavingQueuedEdit,
924929
cancelQueuedRunEdit,

‎apps/server/scripts/acp-mock-agent.ts‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ const copilotModesProfile = process.env.T3_ACP_COPILOT_MODES === "1";
2323
const copilotModeId = (name: string) =>
2424
`https://agentclientprotocol.com/protocol/session-modes#${name}`;
2525
const modeOptionCategory = process.env.T3_ACP_MODE_OPTION_CATEGORY ?? "mode";
26+
// Comma-separated command names the mock advertises, so `/compact` support is testable.
27+
const advertisedCommandNames = (process.env.T3_ACP_AVAILABLE_COMMANDS ?? "")
28+
.split(",")
29+
.map((name) => name.trim())
30+
.filter((name) => name.length > 0);
31+
const loadedCommandNames = process.env.T3_ACP_LOADED_COMMANDS?.split(",").filter(Boolean);
2632
const emitToolCalls = process.env.T3_ACP_EMIT_TOOL_CALLS === "1";
2733
const emitInterleavedAssistantToolCalls =
2834
process.env.T3_ACP_EMIT_INTERLEAVED_ASSISTANT_TOOL_CALLS === "1";
@@ -610,6 +616,18 @@ const program = Effect.gen(function* () {
610616
if (antigravityProfile) {
611617
yield* publishAntigravityCommands(sessionId);
612618
}
619+
if (advertisedCommandNames.length > 0) {
620+
yield* agent.client.sessionUpdate({
621+
sessionId,
622+
update: {
623+
sessionUpdate: "available_commands_update",
624+
availableCommands: advertisedCommandNames.map((name) => ({
625+
name,
626+
description: `${name} command`,
627+
})),
628+
},
629+
});
630+
}
613631
return {
614632
sessionId,
615633
configOptions: configOptions(),
@@ -665,6 +683,15 @@ const program = Effect.gen(function* () {
665683
yield* agent.handleLoadSession((request) =>
666684
Effect.gen(function* () {
667685
const requestedSessionId = String(request.sessionId ?? sessionId);
686+
if (loadedCommandNames !== undefined) {
687+
yield* agent.client.sessionUpdate({
688+
sessionId: requestedSessionId,
689+
update: {
690+
sessionUpdate: "available_commands_update",
691+
availableCommands: loadedCommandNames.map((name) => ({ name, description: name })),
692+
},
693+
});
694+
}
668695
if (failLoadSession) {
669696
return yield* AcpError.AcpRequestError.internalError("Mock load session failure");
670697
}

‎apps/server/scripts/record-cursor-agent-sdk-replay-fixture.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,6 @@ const recordCursorReplayCommand = Command.make(
219219
);
220220

221221
Command.run(recordCursorReplayCommand, { version: "0.0.0" }).pipe(
222-
Effect.provide(Layer.mergeAll(CursorSdk.layer, NodeServices.layer)),
222+
Effect.provide(CursorSdk.layer.pipe(Layer.provideMerge(NodeServices.layer))),
223223
NodeRuntime.runMain,
224224
);

‎apps/server/src/cli/doctor.test.ts‎

Lines changed: 85 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,59 @@
1+
import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
12
import * as NodeServices from "@effect/platform-node/NodeServices";
3+
import * as HostProcess from "@t3tools/shared/HostProcess";
24
import { assert, it } from "@effect/vitest";
35
import * as Console from "effect/Console";
46
import * as Effect from "effect/Effect";
7+
import * as FileSystem from "effect/FileSystem";
58
import * as Layer from "effect/Layer";
9+
import * as Path from "effect/Path";
610
import * as TestClock from "effect/testing/TestClock";
711

812
import * as BootService from "../cloud/bootService.ts";
913
import { runDoctor } from "./doctor.ts";
1014
import { DatabaseMaintenance } from "../persistence/DatabaseMaintenance.ts";
15+
import * as ProcessRunner from "../processRunner.ts";
16+
import { acquireServerOwnership } from "../serverOwnership.ts";
17+
import { makePersistedServerRuntimeState } from "../serverRuntimeState.ts";
18+
19+
// An empty home, so no boot service unit installed on this machine is read.
20+
const layerHost = Layer.mergeAll(
21+
ProcessRunner.layer,
22+
Layer.effect(
23+
HostProcess.HomeDirectory,
24+
Effect.gen(function* () {
25+
return yield* (yield* FileSystem.FileSystem).makeTempDirectoryScoped();
26+
}),
27+
),
28+
Layer.succeed(HostProcess.Environment, {}),
29+
).pipe(Layer.provideMerge(Layer.mergeAll(NodeServices.layer, NodeCrypto.layer)));
1130

1231
it.effect(
13-
"--fix reclaims space with the service paused and keeps the V1 database, and --dry-run changes nothing",
32+
"--fix locks the home while the service is paused, names a server that holds it, keeps the V1 database, and --dry-run changes nothing",
1433
() =>
1534
Effect.gen(function* () {
35+
const fs = yield* FileSystem.FileSystem;
36+
const path = yield* Path.Path;
37+
const stateDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-doctor-" });
38+
const serverRuntimeStatePath = path.join(stateDir, "server-runtime.json");
1639
const calls: string[] = [];
40+
const services =
41+
yield* Effect.context<Effect.Services<ReturnType<typeof acquireServerOwnership>>>();
42+
// Stands in for a server launched for the same home, for example over SSH.
43+
const startServer = Effect.scoped(acquireServerOwnership(serverRuntimeStatePath)).pipe(
44+
Effect.result,
45+
Effect.map((result) =>
46+
calls.push(result._tag === "Success" ? "server started" : result.failure._tag),
47+
),
48+
Effect.provide(services),
49+
);
1750
const doctor = (dryRun: boolean) =>
18-
runDoctor("/test/statev2.sqlite", { fix: true, dryRun, interactive: false }).pipe(
51+
runDoctor(
52+
{ dbPath: path.join(stateDir, "statev2.sqlite"), serverRuntimeStatePath },
53+
{ fix: true, dryRun, interactive: false },
54+
).pipe(
1955
Effect.provide(
2056
Layer.mergeAll(
21-
NodeServices.layer,
2257
Layer.mock(BootService.BootService)({
2358
withStopped: (operation) =>
2459
Effect.sync(() => calls.push("pause")).pipe(
@@ -35,15 +70,17 @@ it.effect(
3570
legacyDatabaseBytes: 4000,
3671
}),
3772
compact: () =>
38-
Effect.sync(() => {
39-
calls.push("compact");
40-
return {
41-
beforeBytes: 2000,
42-
afterBytes: 1000,
43-
deletedEventCount: 1,
44-
deletedReceiptCount: 0,
45-
};
46-
}),
73+
startServer.pipe(
74+
Effect.map(() => {
75+
calls.push("compact");
76+
return {
77+
beforeBytes: 2000,
78+
afterBytes: 1000,
79+
deletedEventCount: 1,
80+
deletedReceiptCount: 0,
81+
};
82+
}),
83+
),
4784
removeLegacyDatabase: () => Effect.sync(() => calls.push("remove legacy")),
4885
}),
4986
),
@@ -52,8 +89,32 @@ it.effect(
5289
yield* doctor(true);
5390
assert.deepEqual(calls, []);
5491
yield* doctor(false);
55-
assert.deepEqual(calls, ["pause", "compact", "resume"]);
56-
}),
92+
yield* startServer;
93+
assert.deepEqual(calls, [
94+
"pause",
95+
"ServerAlreadyRunningError",
96+
"compact",
97+
"resume",
98+
"server started",
99+
]);
100+
101+
calls.length = 0;
102+
const state = yield* makePersistedServerRuntimeState({
103+
config: { host: undefined, devUrl: undefined },
104+
port: 3773,
105+
});
106+
const refused = yield* Effect.scoped(
107+
acquireServerOwnership(serverRuntimeStatePath).pipe(
108+
Effect.tap((owner) => owner.publish(state)),
109+
Effect.andThen(doctor(false)),
110+
),
111+
).pipe(Effect.flip);
112+
assert.equal(
113+
refused.message,
114+
`Stop T3 Code server process ${state.pid}, which the background service did not start, before repairing the database.`,
115+
);
116+
assert.deepEqual(calls, ["pause", "resume"]);
117+
}).pipe(Effect.provide(layerHost)),
57118
);
58119

59120
it.effect("warns how long reclaiming can take and reports each step with its elapsed time", () =>
@@ -65,10 +126,17 @@ it.effect("warns how long reclaiming can take and reports each step with its ela
65126
["rebuild-search", "1 second"],
66127
["checkpoint", "1 second"],
67128
] as const;
68-
yield* runDoctor("/test/statev2.sqlite", { fix: true, dryRun: false, interactive: false }).pipe(
129+
const stateDir = yield* (yield* FileSystem.FileSystem).makeTempDirectoryScoped();
130+
const path = yield* Path.Path;
131+
yield* runDoctor(
132+
{
133+
dbPath: path.join(stateDir, "statev2.sqlite"),
134+
serverRuntimeStatePath: path.join(stateDir, "server-runtime.json"),
135+
},
136+
{ fix: true, dryRun: false, interactive: false },
137+
).pipe(
69138
Effect.provide(
70139
Layer.mergeAll(
71-
NodeServices.layer,
72140
Layer.mock(BootService.BootService)({
73141
withStopped: (operation) =>
74142
Effect.sync(() => output.push("<pause>")).pipe(
@@ -125,5 +193,5 @@ it.effect("warns how long reclaiming can take and reports each step with its ela
125193
["0:00", "1:05", "12:46", "12:47"],
126194
"each compaction step must print one line with the elapsed time as it starts",
127195
);
128-
}),
196+
}).pipe(Effect.provide(layerHost)),
129197
);

‎apps/server/src/cli/doctor.ts‎

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@ import * as Clock from "effect/Clock";
33
import * as Console from "effect/Console";
44
import * as Effect from "effect/Effect";
55
import * as Layer from "effect/Layer";
6+
import * as Option from "effect/Option";
67
import { Command, Flag, GlobalFlag, Prompt } from "effect/cli";
78

89
import * as BootService from "../cloud/bootService.ts";
910
import * as DatabaseMaintenance from "../persistence/DatabaseMaintenance.ts";
11+
import * as ServerOwnership from "../serverOwnership.ts";
12+
import { isProcessAlive, readPersistedServerRuntimeState } from "../serverRuntimeState.ts";
1013
import { projectLocationFlags, resolveCliAuthConfig } from "./config.ts";
1114
import * as CliService from "./service.ts";
1215

@@ -29,10 +32,37 @@ const COMPACT_STEP_LABELS: Record<DatabaseMaintenance.DatabaseCompactStep, strin
2932
checkpoint: "Flushing the write-ahead log",
3033
};
3134

35+
// Holding the server lock refuses a server launched mid-repair, such as one started over SSH.
36+
// Lock only: acquireServerOwnership would also refuse the older service doctor just stopped.
37+
const lockServerHome = Effect.fn("runDoctor.lockServerHome")(function* (paths: {
38+
readonly dbPath: string;
39+
readonly serverRuntimeStatePath: string;
40+
}) {
41+
yield* ServerOwnership.holdServerOwnershipLock(paths.serverRuntimeStatePath).pipe(
42+
Effect.catchTags({
43+
ServerAlreadyRunningError: Effect.fn("runDoctor.nameServerHolder")(function* (cause) {
44+
const owner = yield* readPersistedServerRuntimeState(paths.serverRuntimeStatePath).pipe(
45+
Effect.map(Option.filter((state) => isProcessAlive(state.pid))),
46+
Effect.orElseSucceed(() => Option.none()),
47+
);
48+
return yield* new DatabaseMaintenance.DatabaseInUseError({
49+
path: paths.dbPath,
50+
cause,
51+
...(Option.isSome(owner) && {
52+
pid: owner.value.pid,
53+
serviceManaged: owner.value.serviceManaged === true,
54+
}),
55+
});
56+
}),
57+
}),
58+
);
59+
});
60+
3261
export const runDoctor = Effect.fn("runDoctor")(function* (
33-
databasePath: string,
62+
paths: { readonly dbPath: string; readonly serverRuntimeStatePath: string },
3463
options: { readonly fix: boolean; readonly dryRun: boolean; readonly interactive?: boolean },
3564
) {
65+
const databasePath = paths.dbPath;
3666
const maintenance = yield* DatabaseMaintenance.DatabaseMaintenance;
3767
const status = yield* maintenance.inspect(databasePath);
3868
yield* Console.log(
@@ -123,19 +153,19 @@ export const runDoctor = Effect.fn("runDoctor")(function* (
123153
);
124154
// Report the repair before restarting, which can fail independently.
125155
yield* service.withStopped(
126-
maintenance
127-
.compact(databasePath, reportStep)
128-
.pipe(
129-
Effect.tap((result) =>
130-
Clock.monotonicTimeNanos.pipe(
131-
Effect.flatMap((now) =>
132-
Console.log(
133-
`Done in ${formatElapsed(now - startedAt)}: ${formatBytes(result.beforeBytes)} -> ${formatBytes(result.afterBytes)}, ${result.deletedEventCount} superseded events removed.`,
134-
),
156+
lockServerHome(paths).pipe(
157+
Effect.andThen(maintenance.compact(databasePath, reportStep)),
158+
Effect.tap((result) =>
159+
Clock.monotonicTimeNanos.pipe(
160+
Effect.flatMap((now) =>
161+
Console.log(
162+
`Done in ${formatElapsed(now - startedAt)}: ${formatBytes(result.beforeBytes)} -> ${formatBytes(result.afterBytes)}, ${result.deletedEventCount} superseded events removed.`,
135163
),
136164
),
137165
),
138166
),
167+
Effect.scoped,
168+
),
139169
);
140170
}
141171
if (selected.includes("legacy")) {
@@ -163,7 +193,7 @@ export const doctorCommand = Command.make("doctor", {
163193
Effect.gen(function* () {
164194
const logLevel = yield* GlobalFlag.LogLevel;
165195
const config = yield* resolveCliAuthConfig(flags, logLevel);
166-
yield* runDoctor(config.dbPath, { fix: flags.fix, dryRun: flags.dryRun }).pipe(
196+
yield* runDoctor(config, { fix: flags.fix, dryRun: flags.dryRun }).pipe(
167197
Effect.provide(Layer.merge(DatabaseMaintenance.layer, CliService.layer(config))),
168198
);
169199
}),

0 commit comments

Comments
 (0)