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
20 changes: 16 additions & 4 deletions .agents/skills/sync-upstream/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,10 @@ git merge --no-ff upstream/main
Resolve every conflict so both sides survive: upstream's change and the fork's behaviour or text.
When upstream rewrote code the fork had changed, carry the fork's intent into upstream's new shape,
including its conventions (for example `Effect.fn` in place of plain arrow functions). Resolving a
whole file to one side is how fork changes go missing. Done when
`git diff --name-only --diff-filter=U` prints nothing and the merge is committed.
whole file to one side is how fork changes go missing. Chris wants no comments in code unless
absolutely needed, so a fork comment that conflicts or that upstream's comment lint rejects is
deleted, not trimmed. Done when `git diff --name-only --diff-filter=U` prints nothing and the merge
is committed.

### 3. Prove fork changes survived

Expand Down Expand Up @@ -68,15 +70,19 @@ it separately: `chore(release): bump PseudoCode to <version>`.

```sh
vp run check:changed
vp run lint
.agents/skills/sync-upstream/scripts/run-suites.sh
```

`vp run lint` is CI's whole-repo lint. A sync brings upstream's new lint rules, and they fail fork
files the merge never touched, which `check:changed` does not read.

`run-suites.sh` runs every package's tests one at a time, the one place in this repo where the full
suites run locally: Chris needs a sync green before it ships. Fix each failure at its cause. When
you believe a failure belongs to this machine rather than the code, prove it: run the same test in
a clean upstream worktree (`git worktree add ../t3-upstream-check upstream/main`, then `vp i`
there). If it fails there too, report it with that evidence as a failure, never as a pass, and
remove the worktree. Done when both commands pass, or every remaining failure carries that proof.
remove the worktree. Done when all 3 commands pass, or every remaining failure carries that proof.

### 6. Check the app live

Expand All @@ -89,7 +95,13 @@ merge touched. Done when each surface was driven and what you saw is written dow
Push the branch to `origin` and open a PR into the fork's `main`. The body names the upstream
commit merged, the fork changes restored, each DROPPED line accepted and why, the test counts, and
what the live check covered. Chris merges it with a **merge commit**; a squash would orphan the
commits the release tag points at. Done when the PR is open. Stop there until Chris merges.
commits the release tag points at.

The PR's CI must pass. This Mac is arm64 and CI runs on x64 Linux, so some failures only show up
there; fix them at their cause like any other. A job GitHub cancels with "not acquired by Runner"
never got a macOS runner: rerun it with
`gh run rerun <run-id> --failed --repo pseudoseed/t3code_dev`. Done when the PR is open and every CI
job passes. Stop there until Chris merges.

### 8. Release from merged main

Expand Down
2 changes: 2 additions & 0 deletions apps/mobile/src/features/agent-awareness/AgentWidgetSync.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,13 @@ function IosAgentWidgetSync() {
// entry is dated from the last write, and a quiet, healthy app must not
// let the widget claim its updates are late.
const [refreshTick, setRefreshTick] = useState(0);
// oxlint-disable-next-line t3code/no-unreasoned-state-sync -- runs the timer that forces a periodic widget write
useEffect(() => {
const timer = setInterval(() => setRefreshTick((tick) => tick + 1), WIDGET_REFRESH_MS);
return () => clearInterval(timer);
}, []);

// oxlint-disable-next-line t3code/no-unreasoned-state-sync -- writes the snapshot to the native widget's shared storage
useEffect(() => {
// Register the gallery layout even before a server snapshot is available.
if (canPublish && previousContent.current === contentKey && refreshTick === lastTick.current)
Expand Down
73 changes: 39 additions & 34 deletions apps/mobile/src/features/agent-awareness/DirectPushCoordinator.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useAtomValue } from "@effect/atom-react";
import { Atom, AsyncResult } from "effect/reactivity";
import { useEffect, useState } from "react";
import { useEffect, useEffectEvent, useState } from "react";
import { AppState, Platform } from "react-native";
import * as Notifications from "expo-notifications";
import type { DirectPushDelivery, EnvironmentId } from "@t3tools/contracts";
Expand Down Expand Up @@ -62,6 +62,7 @@ function Coordinator() {
]),
);

// oxlint-disable-next-line t3code/no-unreasoned-state-sync -- listens for app foregrounding and push token rotation
useEffect(() => {
const app = AppState.addEventListener("change", (state) => {
if (state === "active") setRefresh((n) => n + 1);
Expand All @@ -74,6 +75,7 @@ function Coordinator() {
tokens.remove();
};
}, []);
// oxlint-disable-next-line t3code/no-unreasoned-state-sync -- installs the native notification handler while direct push is on
useEffect(() => {
if (!enabled) return;
Notifications.setNotificationHandler({
Expand All @@ -87,6 +89,7 @@ function Coordinator() {
return () => Notifications.setNotificationHandler(null);
}, [enabled]);

// oxlint-disable-next-line t3code/no-unreasoned-state-sync -- keeps native Live Activities and the server's preference in step with the direct push setting
useEffect(() => {
if (!chosen) return;
if (enabled) endLocalLiveActivities("Could not close the Cloud Connect Live Activity.");
Expand All @@ -97,11 +100,7 @@ function Coordinator() {
});
}, [enabled, chosen]);

// Registration follows the socket, not the foreground: a reconnect while
// backgrounded (or a token rotated while suspended) must still reach the
// server, or the widget's push path stays dead until the next open.
useEffect(() => {
if (!chosen || enabled) return;
const disableConnected = useEffectEvent(() => {
for (const environment of environments) {
if (environment.connection.phase === "connected") {
void disableDirectPush(environment.environmentId)
Expand All @@ -111,43 +110,49 @@ function Coordinator() {
);
}
}
}, [connectionKey, chosen, enabled, refresh]);

});
// oxlint-disable-next-line t3code/no-unreasoned-state-sync -- turns direct push off on each server as it connects, backgrounded or not
useEffect(() => {
if (!loaded || !enabled) return;
if (!chosen || enabled) return;
disableConnected();
// oxlint-disable-next-line react/exhaustive-effect-dependencies -- connection changes and app foregrounding repeat the disable
}, [connectionKey, chosen, enabled, refresh]);

let cancelled = false;
// Coalesce bursts from shell hydration and provider lifecycle changes.
const timer = setTimeout(() => {
for (const environment of environments) {
if (environment.connection.phase !== "connected") continue;
const id = environment.environmentId;
setStatus(id, "Registering…");
void syncDirectPush(id, liveEnabled)
.then((status) => {
if (cancelled) return;
setDelivery(id, status?.lastDelivery);
const registerConnected = useEffectEvent((isCancelled: () => boolean) => {
for (const environment of environments) {
if (environment.connection.phase !== "connected") continue;
const id = environment.environmentId;
setStatus(id, "Registering…");
void syncDirectPush(id, liveEnabled)
.then((status) => {
if (isCancelled()) return;
setDelivery(id, status?.lastDelivery);
setStatus(
id,
status?.registered
? "Registered with Apple notification delivery"
: "APNs setup needed on this server",
);
})
.catch((error: unknown) => {
if (!isCancelled())
setStatus(
id,
status?.registered
? "Registered with Apple notification delivery"
: "APNs setup needed on this server",
error instanceof Error ? error.message : "Registration failed. Reconnect to retry.",
);
})
.catch((error: unknown) => {
if (!cancelled)
setStatus(
id,
error instanceof Error ? error.message : "Registration failed. Reconnect to retry.",
);
});
}
}, 500);
});
}
});
// oxlint-disable-next-line t3code/no-unreasoned-state-sync -- registers each connected server for direct push, coalescing bursts of shell updates
useEffect(() => {
if (!loaded || !enabled) return;
let cancelled = false;
const timer = setTimeout(() => registerConnected(() => cancelled), 500);
return () => {
cancelled = true;
clearTimeout(timer);
};
// The signatures exclude incidental object changes from shell updates.
// oxlint-disable-next-line react/exhaustive-effect-dependencies -- connection, thread activity, setting and foreground changes repeat the registration
}, [connectionKey, activityKey, enabled, liveEnabled, loaded, refresh]);
return null;
}
Expand Down
1 change: 1 addition & 0 deletions apps/mobile/src/widgets/pseudocode/ProjectIconSync.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export function ProjectIconSync({
}),
);
const key = `${project.environmentId}:${project.id}`;
// oxlint-disable-next-line t3code/no-unreasoned-state-sync -- downscales the project favicon for the native widget and aborts the download on cleanup
useEffect(() => {
const controller = new AbortController();
if (url && !isProjectFaviconFallbackUrl(url))
Expand Down
7 changes: 0 additions & 7 deletions apps/server/src/diagnostics/EventLoopStallMonitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,6 @@ import * as Duration from "effect/Duration";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";

/**
* Notices when the process stopped being scheduled. A one-second timer that
* fires many seconds late means the host suspended, napped, or starved this
* process, which from a remote client looks like a server that ignores
* requests. Each stall becomes a span so the trace log carries proof of when
* and for how long, next to the connection churn it caused.
*/
interface EventLoopStallMonitorOptions {
readonly intervalMs: number;
readonly thresholdMs: number;
Expand Down
17 changes: 0 additions & 17 deletions apps/server/src/provider/providerCredentialHome.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,3 @@
/**
* Per-instance credential directories.
*
* A provider CLI keeps one signed-in account per credential directory, so two
* instances of the same provider need two directories or the second sign-in
* overwrites the first. Users should not have to know that, so PseudoCode
* provisions a directory for any instance that does not already have one.
*
* The rule has one deliberate exception. The instance whose id equals its
* driver kind is the one migrated from the single-instance world; it keeps
* pointing at the provider's own default home (`~/.claude`, `~/.codex`) so an
* existing sign-in, and everything else the user has in that directory, keeps
* working untouched. Every additional instance is new by definition and gets
* its own directory.
*
* @module provider/providerCredentialHome
*/
import {
defaultInstanceIdForDriver,
type ProviderDriverKind,
Expand Down
11 changes: 0 additions & 11 deletions apps/server/src/terminal/shell-integration/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,3 @@
/**
* Wires OSC 133 shell integration into terminal spawns.
*
* The terminal renders whatever the shell sends, and a default zsh or bash
* prompt sends no styling at all. We give stock zsh prompts a compact colored
* layout and preserve custom prompts. OSC 133 markers identify prompt, input,
* and output boundaries for libghostty-vt.
*
* Supported for zsh and bash on POSIX platforms. Every other shell spawns
* untouched.
*/
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";

Expand Down
13 changes: 1 addition & 12 deletions apps/server/src/usage/aiUsageApi.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,3 @@
/**
* aiUsageApi — the `aiusage` source kind: a local AI usage dashboard that
* polls each vendor's own limits endpoint. It exposes the same
* `readAccounts` / `consume` pair as {@link ./cliproxyApi.ts}, so
* UsageLimitSources only has to pick an adapter by `config.kind`.
*
* A dashboard runs on this machine and authenticates nothing, so there is no
* management key and no per-account credential handling here.
*
* @module usage/aiUsageApi
*/
import {
UsageLimitSourceError,
type ProviderConsumeResetCreditResult,
Expand Down Expand Up @@ -51,7 +40,7 @@ const failureDetail = (error: { readonly _tag: string }): string => {
}
};

/** @public Service construction is part of the canonical Effect module API. */
/** @public */
export const makeAiUsageApi = Effect.gen(function* () {
const client = yield* HttpClient.HttpClient;

Expand Down
17 changes: 0 additions & 17 deletions apps/server/src/usage/aiUsageDashboard.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,3 @@
/**
* Maps an AI usage dashboard's `/api/usage` response onto the usage limit
* windows the Limits view renders, one row per account it polls.
*
* The dashboard groups accounts by company and hands each one a flat list of
* meters it has already normalised from the vendor's own limits endpoint:
* `{ id, group, name, percent, resets_at }`, where `group` is exactly the
* window kinds this contract uses. So the mapping is mostly a rename, plus a
* driver lookup for the icon and Codex's banked reset credits.
*
* Unlike a CLIProxyAPI hub, the dashboard reports accounts this environment
* is not signed into. An account it could not read keeps its row with the
* failure on it rather than disappearing, matching how the dashboard's own
* cards behave.
*
* @module usage/aiUsageDashboard
*/
import {
ProviderDriverKind,
type ServerProviderUsageLimits,
Expand Down
12 changes: 0 additions & 12 deletions apps/server/src/wsKeepAlive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,6 @@ export interface WebSocketKeepAliveOptions {
readonly missesBeforeClose: number;
}

/**
* Server-side liveness for the Node `ws` path. A phone that gets suspended,
* changes networks, or loses its NAT mapping never sends a close frame;
* without this the connection, its subscriptions, and its session's
* "connected" count outlive the device indefinitely. `ws` answers pings at
* the protocol level without waking the client's JavaScript, so a healthy
* but idle client costs one frame per interval and a dead one is reaped after
* `missesBeforeClose` silent intervals. Any inbound frame counts as life.
*
* Returned as a `WebSocket` subclass because that is the only per-connection
* hook `ws` exposes through `WebSocketServer` options.
*/
export function makeKeepAliveWebSocket(
WebSocketClass: typeof WsTypes.WebSocket,
options: WebSocketKeepAliveOptions,
Expand Down
4 changes: 3 additions & 1 deletion apps/web/src/components/AgentAttentionCoordinator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ function EnvironmentAttentionObserver({ environmentId }: { environmentId: Enviro
const ref = scopeThreadRef(environmentId, request.threadId);
try {
const notification = showAgentAttentionNotification({
title: `${APP_DISPLAY_NAME} — ${request.kind === "approval" ? "Approval needed" : "Input needed"}`,
title: `${APP_DISPLAY_NAME}: ${request.kind === "approval" ? "Approval needed" : "Input needed"}`,
body: request.title,
tag: `agent-attention:${scopedThreadKey(ref)}`,
onClick: () => {
Expand All @@ -53,6 +53,7 @@ function EnvironmentAttentionObserver({ environmentId }: { environmentId: Enviro
return delivered;
});

// oxlint-disable-next-line t3code/no-unreasoned-state-sync -- subscribes to the environment shell atom and closes the system notifications it raised on cleanup
useEffect(() => {
const track = createAgentAttentionTracker();
const notifications = new Map<ThreadId, Notification>();
Expand Down Expand Up @@ -103,6 +104,7 @@ function EnvironmentAttentionObserver({ environmentId }: { environmentId: Enviro
export function AgentAttentionCoordinator() {
const catalog = useAtomValue(environmentCatalog.catalogValueAtom);
const soundEnabled = useClientSettings((settings) => settings.agentAttentionSound);
// oxlint-disable-next-line t3code/no-unreasoned-state-sync -- holds window gesture listeners that unlock browser audio while the sound setting is on
useEffect(() => {
if (!soundEnabled || isElectron) return;
const removeListeners = () => {
Expand Down
Loading
Loading