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
76 changes: 75 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,78 @@ jobs:
- name: Check packaging contract
run: pnpm test:packaging

legacy-bridge-continuity:
runs-on: ubuntu-24.04
timeout-minutes: 20
steps:
- name: Check out exact T4 source
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}

- name: Install pnpm
uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
with:
version: 11.10.0

- name: Install Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 24.13.1
cache: pnpm

- name: Install Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: 1.3.14

- name: Install continuity prerequisites
run: sudo apt-get update && sudo apt-get install --yes expect

- name: Install T4 dependencies
run: pnpm install --frozen-lockfile

- name: Resolve pinned OMP authority source
id: authority
shell: bash
run: |
set -euo pipefail
source_repository="$(jq -er '.sourceRepository' provenance/omp-host-migration.json)"
test "$source_repository" = "https://github.com/lyc-aon/oh-my-pi"
sha="$(jq -er '.inputs.operationsContinuity' provenance/omp-host-migration.json)"
[[ "$sha" =~ ^[0-9a-f]{40}$ ]]
echo "repository=lyc-aon/oh-my-pi" >> "$GITHUB_OUTPUT"
echo "sha=$sha" >> "$GITHUB_OUTPUT"

- name: Check out pinned OMP authority source
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
repository: ${{ steps.authority.outputs.repository }}
ref: ${{ steps.authority.outputs.sha }}
path: .continuity/omp

- name: Install OMP authority dependencies
working-directory: .continuity/omp
run: bun install --frozen-lockfile

- name: Build OMP native addon
working-directory: .continuity/omp
run: bun run build:native

- name: Run legacy bridge continuity gate
env:
T4_OMP_SOURCE_DIR: ${{ github.workspace }}/.continuity/omp
run: pnpm test:legacy-bridge-continuity

- name: Upload continuity evidence
if: ${{ always() }}
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: legacy-bridge-continuity-${{ github.run_id }}
path: artifacts/legacy-bridge-continuity/
if-no-files-found: error
retention-days: 14

tooling:
runs-on: ubuntu-24.04
timeout-minutes: 25
Expand Down Expand Up @@ -124,18 +196,20 @@ jobs:
verify:
name: verify
if: ${{ always() }}
needs: [core, tooling, android-debug]
needs: [core, legacy-bridge-continuity, tooling, android-debug]
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- name: Require every CI leg
shell: bash
env:
CORE_RESULT: ${{ needs.core.result }}
CONTINUITY_RESULT: ${{ needs.legacy-bridge-continuity.result }}
TOOLING_RESULT: ${{ needs.tooling.result }}
ANDROID_RESULT: ${{ needs.android-debug.result }}
run: |
set -euo pipefail
test "$CORE_RESULT" = success
test "$CONTINUITY_RESULT" = success
test "$TOOLING_RESULT" = success
test "$ANDROID_RESULT" = success
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ dist-electron/
!.env.example

# Test and runtime output
/artifacts/operations-continuity/
/artifacts/legacy-bridge-continuity/
/.continuity/
/evidence/wire/
coverage/
playwright-report/
Expand Down
20 changes: 14 additions & 6 deletions apps/web/src/features/session-runtime/live-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import { pendingPromptsFromRef } from "./pending-prompts.ts";
import {
createTranscriptArtifactSource,
type TranscriptImageAvailability,
type TranscriptMediaReference,
} from "./transcript-images.ts";
import {
commandSupport,
Expand Down Expand Up @@ -300,24 +301,30 @@ export function createLiveSessionRuntime(options: LiveRuntimeOptions): SessionRu

const transcriptImageAvailability = (
runtime: DesktopRuntimeSnapshot,
reference: TranscriptMediaReference,
): TranscriptImageAvailability => {
if (runtime.connections.get(targetId) !== "connected") {
return { available: false, reason: "Reconnect to this host to load transcript images." };
return { available: false, reason: "Reconnect to this host to load transcript media." };
}
const host = runtime.hosts.get(options.hostId);
const noun = "source" in reference ? "artifact" : "transcript image";
if (host === undefined || !host.grantedCapabilities.includes("sessions.read")) {
return {
available: false,
reason: "This target does not grant transcript image access.",
reason: `This target does not grant ${noun} access.`,
};
}
if (
!host.grantedFeatures.includes("transcript.images") &&
!host.grantedFeatures.includes("artifacts.read")
"source" in reference
? !host.grantedFeatures.includes("artifacts.read")
: !host.grantedFeatures.includes("transcript.images")
) {
return {
available: false,
reason: "This OMP host does not offer transcript image reads.",
reason:
"source" in reference
? "This host does not offer artifact reads."
: "This OMP host does not offer transcript image reads.",
};
}
if (!transcriptImagesAttached) {
Expand All @@ -327,9 +334,10 @@ export function createLiveSessionRuntime(options: LiveRuntimeOptions): SessionRu
};

const syncTranscriptImageAvailability = (runtime: DesktopRuntimeSnapshot) => {
transcriptImages.setAvailability(transcriptImageAvailability(runtime));
transcriptImages.setAvailability((reference) => transcriptImageAvailability(runtime, reference));
};


const warmSession = (runtime: DesktopRuntimeSnapshot): SessionProjection | undefined =>
runtime.projection.sessions.get(projectionKey);

Expand Down
78 changes: 49 additions & 29 deletions apps/web/src/features/session-runtime/transcript-images.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ export type TranscriptImageAvailability =
| { readonly available: true }
| { readonly available: false; readonly reason: string };

export type TranscriptMediaAvailability =
| TranscriptImageAvailability
| ((reference: TranscriptMediaReference) => TranscriptImageAvailability);

export type TranscriptImageSnapshot =
| { readonly status: "loading" }
| {
Expand Down Expand Up @@ -74,7 +78,7 @@ interface TranscriptMediaSourceOptions<TReference extends TranscriptMediaReferen
offset: number,
signal: TranscriptImageReadSignal,
) => Promise<TranscriptImageCommandResult>;
readonly availability?: TranscriptImageAvailability;
readonly availability?: TranscriptMediaAvailability;
readonly hostId?: string;
readonly sessionId?: string;
readonly maxCacheBytes?: number;
Expand Down Expand Up @@ -622,9 +626,9 @@ export class ManagedTranscriptImageSource implements TranscriptImageSource {
private readonly sessionId: string | undefined;
private readonly entries = new Map<string, CacheEntry>();
private readonly pendingLoads: CacheEntry[] = [];
private availability: TranscriptImageAvailability;
private unavailable: TranscriptImageSnapshot | null;
private availability: TranscriptMediaAvailability;
private readonly pausedSnapshot = unavailableSnapshot(TRANSCRIPT_IMAGE_PAUSED_REASON);
private readonly unavailableSnapshots = new Map<string, TranscriptImageSnapshot>();
private paused = false;
private disposedReason: string | null = null;
private disposedSnapshot: TranscriptImageSnapshot | null = null;
Expand Down Expand Up @@ -660,57 +664,59 @@ export class ManagedTranscriptImageSource implements TranscriptImageSource {
available: false,
reason: TRANSCRIPT_IMAGE_FIXTURE_REASON,
};
this.unavailable = this.availability.available
? null
: unavailableSnapshot(this.availability.reason);
this.hostId = options.hostId;
this.sessionId = options.sessionId;
if (this.hostId !== undefined && this.sessionId !== undefined) {
registerSource(this.hostId, this.sessionId, this);
}
}

setAvailability(availability: TranscriptImageAvailability): void {
setAvailability(availability: TranscriptMediaAvailability): void {
if (this.disposedReason !== null) return;
const unchanged =
this.availability.available === availability.available &&
(this.availability.available ||
(!availability.available && this.availability.reason === availability.reason));
if (unchanged) return;
const becameAvailable = !this.availability.available && availability.available;
const previous = this.availability;
this.availability = availability;
this.unavailable = availability.available ? null : unavailableSnapshot(availability.reason);
for (const entry of this.entries.values()) {
if (!availability.available) {
const before = this.availabilityFor(entry.reference, previous);
const after = this.availabilityFor(entry.reference);
if (!after.available) {
if (entry.state === "queued") this.cancelQueued(entry);
if (entry.state === "waiting") {
entry.state = "idle";
entry.snapshot = null;
}
if (entry.state === "loading") this.cancelActive(entry);
}
if (becameAvailable && entry.state === "error" && entry.retryable) {
if (!before.available && after.available && entry.state === "error" && entry.retryable) {
entry.state = "idle";
entry.snapshot = null;
}
this.notify(entry);
if (!this.paused && availability.available && entry.refCount > 0 && entry.state === "idle") {
if (!this.paused && after.available && entry.refCount > 0 && entry.state === "idle") {
this.start(entry);
}
}
if (!this.paused && availability.available) this.drainQueue();
if (!this.paused) this.drainQueue();
}

getSnapshot(reference: TranscriptMediaReference): TranscriptImageSnapshot {
if (this.disposedSnapshot !== null) return this.disposedSnapshot;
if (this.paused) return this.pausedSnapshot;
const entry = this.entries.get(mediaKey(reference));
const availability = this.availabilityFor(reference);
if (!availability.available) return this.unavailableSnapshot(availability.reason);
if (entry?.state === "ready" && entry.snapshot !== null) return entry.snapshot;
if (this.unavailable !== null) return this.unavailable;
if (entry?.state === "error" && entry.snapshot !== null) return entry.snapshot;
return LOADING_SNAPSHOT;
}

private unavailableSnapshot(reason: string): TranscriptImageSnapshot {
const existing = this.unavailableSnapshots.get(reason);
if (existing !== undefined) return existing;
const snapshot = unavailableSnapshot(reason);
this.unavailableSnapshots.set(reason, snapshot);
return snapshot;
}

subscribe(reference: TranscriptMediaReference, listener: () => void): () => void {
if (this.disposedReason !== null) return () => undefined;
const entry = this.ensureEntry(reference);
Expand All @@ -732,7 +738,7 @@ export class ManagedTranscriptImageSource implements TranscriptImageSource {
entry.snapshot = null;
this.notify(entry);
}
if (this.availability.available && entry.state === "idle") this.start(entry);
if (this.availabilityFor(entry.reference).available && entry.state === "idle") this.start(entry);
let retained = true;
return () => {
if (!retained) return;
Expand Down Expand Up @@ -810,11 +816,15 @@ export class ManagedTranscriptImageSource implements TranscriptImageSource {
this.paused = false;
for (const entry of this.entries.values()) {
this.notify(entry);
if (this.availability.available && entry.refCount > 0 && entry.state === "idle") {
if (
this.availabilityFor(entry.reference).available &&
entry.refCount > 0 &&
entry.state === "idle"
) {
this.start(entry);
}
}
if (this.availability.available) this.drainQueue();
this.drainQueue();
}

dispose(reason = "Transcript image cache was closed."): void {
Expand Down Expand Up @@ -870,6 +880,12 @@ export class ManagedTranscriptImageSource implements TranscriptImageSource {
this.entries.set(key, entry);
return entry;
}
private availabilityFor(
reference: TranscriptMediaReference,
availability = this.availability,
): TranscriptImageAvailability {
return typeof availability === "function" ? availability(reference) : availability;
}

private touch(entry: CacheEntry): void {
this.clock += 1;
Expand All @@ -881,9 +897,13 @@ export class ManagedTranscriptImageSource implements TranscriptImageSource {
}

private retryCapacityWaiters(): void {
if (this.disposedReason !== null || this.paused || !this.availability.available) return;
if (this.disposedReason !== null || this.paused) return;
for (const entry of this.entries.values()) {
if (entry.refCount > 0 && entry.state === "waiting") {
if (
this.availabilityFor(entry.reference).available &&
entry.refCount > 0 &&
entry.state === "waiting"
) {
entry.state = "idle";
entry.snapshot = null;
this.notify(entry);
Expand All @@ -896,7 +916,7 @@ export class ManagedTranscriptImageSource implements TranscriptImageSource {
if (
this.disposedReason !== null ||
this.paused ||
!this.availability.available ||
!this.availabilityFor(entry.reference).available ||
entry.refCount <= 0 ||
entry.state !== "idle" ||
entry.promise !== null ||
Expand All @@ -914,12 +934,13 @@ export class ManagedTranscriptImageSource implements TranscriptImageSource {
}

private drainQueue(): void {
if (this.disposedReason !== null || this.paused || !this.availability.available) return;
if (this.disposedReason !== null || this.paused) return;
while (this.activeLoads < this.maxConcurrentLoads && this.pendingLoads.length > 0) {
const entry = this.pendingLoads.shift();
if (
entry === undefined ||
this.entries.get(entry.key) !== entry ||
!this.availabilityFor(entry.reference).available ||
entry.refCount <= 0 ||
entry.state !== "queued" ||
!entry.queued
Expand All @@ -941,7 +962,7 @@ export class ManagedTranscriptImageSource implements TranscriptImageSource {
if (
this.disposedReason === null &&
!this.paused &&
this.availability.available &&
this.availabilityFor(entry.reference).available &&
this.entries.get(entry.key) === entry &&
entry.refCount > 0 &&
entry.state === "idle"
Expand Down Expand Up @@ -1096,8 +1117,7 @@ export class ManagedTranscriptImageSource implements TranscriptImageSource {
if (chunks > TRANSCRIPT_IMAGE_MAX_CHUNKS) {
throw new TranscriptImageFailure(TRANSCRIPT_IMAGE_PROTOCOL_ERROR, false);
}
this.assertCurrent(entry, signal);
if (!this.availability.available) {
if (!this.availabilityFor(entry.reference).available) {
throw new TranscriptImageFailure(TRANSCRIPT_IMAGE_LOAD_ERROR, true);
}
let response: TranscriptImageCommandResult;
Expand Down
Loading
Loading