diff --git a/.github/workflows/desktop-macos-preview.yml b/.github/workflows/desktop-macos-preview.yml index 6d1264aa7..7875aec6f 100644 --- a/.github/workflows/desktop-macos-preview.yml +++ b/.github/workflows/desktop-macos-preview.yml @@ -2,25 +2,38 @@ name: Desktop macOS Preview on: pull_request: - types: [labeled, synchronize, reopened] + types: [labeled, unlabeled, synchronize, reopened, closed] permissions: contents: read - pull-requests: write +# Build events and cleanup events use separate groups: a push must cancel a +# stale in-flight build, but must never cancel a cleanup run mid-delete. The +# publish job re-checks PR state before uploading to cover the reverse race. concurrency: - group: desktop-macos-preview-${{ github.event.pull_request.number }} - cancel-in-progress: true + group: desktop-macos-preview-${{ github.event.pull_request.number }}-${{ contains(fromJSON('["closed", "unlabeled"]'), github.event.action) && 'cleanup' || 'build' }} + # Cleanup runs must complete (a close event right after an unlabel queues + # behind the running cleanup instead of canceling it mid-delete), and events + # that skip the build job, such as adding an unrelated label, must not + # cancel an in-flight build either. + cancel-in-progress: ${{ !contains(fromJSON('["closed", "unlabeled"]'), github.event.action) && (github.event.action != 'labeled' || github.event.label.name == 'preview:mac') }} jobs: + # Builds run PR code, so this job keeps a read-only token. Publishing to the + # release happens in the publish job below, which never checks out PR code. build: name: Build macOS Apple Silicon preview if: >- + github.event.action != 'closed' && + github.event.action != 'unlabeled' && github.event.pull_request.head.repo.full_name == github.repository && contains(github.event.pull_request.labels.*.name, 'preview:mac') && (github.event.action != 'labeled' || github.event.label.name == 'preview:mac') runs-on: blacksmith-12vcpu-macos-26 timeout-minutes: 30 + outputs: + dmg_name: ${{ steps.build.outputs.dmg_name }} + version: ${{ steps.version.outputs.version }} steps: - name: Checkout uses: actions/checkout@v6 @@ -93,8 +106,9 @@ jobs: fi printf 'dmg_name=%s\n' "$(basename "${dmg_files[0]}")" >> "$GITHUB_OUTPUT" - - id: upload - name: Upload macOS DMG + # archive: false uploads the file as its own artifact named after the + # file, so the publish job downloads by *.dmg pattern, not by name. + - name: Upload macOS DMG uses: actions/upload-artifact@v7 with: path: release/*.dmg @@ -103,13 +117,112 @@ jobs: overwrite: true retention-days: 7 + # Release assets download without a GitHub account, unlike workflow + # artifacts. All preview DMGs live on one rolling prerelease tagged + # "desktop-preview" (release.yml only matches v*.*.* tags), so publishing a + # build never notifies release watchers. This job holds the write token and + # only handles the artifact the build job produced; it never runs PR code. + publish: + name: Publish anonymous download + needs: build + runs-on: blacksmith-8vcpu-ubuntu-2404 + timeout-minutes: 10 + permissions: + contents: write + pull-requests: write + steps: + - name: Download macOS DMG + uses: actions/download-artifact@v8 + with: + pattern: "*.dmg" + merge-multiple: true + path: release + + - id: upload + name: Upload DMG to the rolling preview release + shell: bash + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ github.event.pull_request.number }} + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + run: | + set -euo pipefail + + tag="desktop-preview" + + # True while the PR is open and still carries the preview label. + preview_eligible() { + [[ "$(gh pr view "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" \ + --json state,labels \ + --jq '.state + " " + (.labels | map(.name) | contains(["preview:mac"]) | tostring)')" == "OPEN true" ]] + } + + # The build ran for many minutes. If the PR closed or lost the label + # meanwhile, cleanup already ran in its own concurrency group, so + # publishing now would resurrect a deleted download. + if ! preview_eligible; then + echo "PR closed or preview label removed while building. Skipping publish." + exit 0 + fi + + dmg_path="$(find release -type f -name '*.dmg' -print -quit)" + if [[ -z "$dmg_path" ]]; then + echo "No DMG found in the downloaded artifact." >&2 + exit 1 + fi + + # The filename comes out of the build, which runs PR code. Requiring + # this PR's marker keeps a build from clobbering or deleting another + # PR's asset, since those names carry a different -pr.N. marker. + if [[ "$(basename "$dmg_path")" != *"-pr.${PR_NUMBER}."* ]]; then + echo "DMG name '$(basename "$dmg_path")' does not carry this PR's -pr.${PR_NUMBER}. marker. Refusing to publish." >&2 + exit 1 + fi + + if ! gh release view "$tag" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then + # "|| true" tolerates a concurrent publish job creating the + # release between the check and the create. + gh release create "$tag" \ + --repo "$GITHUB_REPOSITORY" \ + --target "$DEFAULT_BRANCH" \ + --prerelease \ + --title "Desktop preview builds" \ + --notes "Rolling unsigned desktop builds from pull requests with a preview label. Each download is removed when its pull request closes or loses the label. Install stable builds from the latest release instead." \ + || true + fi + + # Keep one DMG per PR: drop this PR's older builds first. The + # trailing dot keeps -pr.12. from matching -pr.123. builds. + gh release view "$tag" --repo "$GITHUB_REPOSITORY" --json assets --jq '.assets[].name' \ + | { grep -F -- "-pr.${PR_NUMBER}." || true; } \ + | while read -r asset; do + gh release delete-asset "$tag" "$asset" --repo "$GITHUB_REPOSITORY" --yes \ + || echo "Asset $asset was already removed by a concurrent run." + done + + gh release upload "$tag" "$dmg_path" --repo "$GITHUB_REPOSITORY" --clobber + + # Re-check after uploading. A cleanup run that started during the + # upload listed assets before ours existed, so it cannot delete it. + # Whichever writer acts last sees the final PR state; if the preview + # became ineligible, delete what we just uploaded. + if ! preview_eligible; then + gh release delete-asset "$tag" "$(basename "$dmg_path")" --repo "$GITHUB_REPOSITORY" --yes \ + || echo "Asset was already removed by a concurrent run." + echo "PR closed or preview label removed during upload. Removed the download." + exit 0 + fi + + echo "download_url=https://github.com/${GITHUB_REPOSITORY}/releases/download/${tag}/$(basename "$dmg_path")" >> "$GITHUB_OUTPUT" + - name: Comment download link + if: steps.upload.outputs.download_url != '' uses: actions/github-script@v8 env: - ARTIFACT_URL: ${{ steps.upload.outputs.artifact-url }} - DMG_NAME: ${{ steps.build.outputs.dmg_name }} + DOWNLOAD_URL: ${{ steps.upload.outputs.download_url }} + DMG_NAME: ${{ needs.build.outputs.dmg_name }} HEAD_SHA: ${{ github.event.pull_request.head.sha }} - PREVIEW_VERSION: ${{ steps.version.outputs.version }} + PREVIEW_VERSION: ${{ needs.build.outputs.version }} with: script: | const { data: pullRequest } = await github.rest.pulls.get({ @@ -117,7 +230,11 @@ jobs: repo: context.repo.repo, pull_number: context.payload.pull_request.number, }); - if (pullRequest.head.sha !== process.env.HEAD_SHA) { + if ( + pullRequest.head.sha !== process.env.HEAD_SHA || + pullRequest.state !== "open" || + !pullRequest.labels.some((label) => label.name === "preview:mac") + ) { core.info("Skipping the outdated macOS preview comment."); return; } @@ -127,7 +244,7 @@ jobs: marker, "### macOS preview", "", - `[Download Apple Silicon DMG](${process.env.ARTIFACT_URL})`, + `[Download Apple Silicon DMG](${process.env.DOWNLOAD_URL})`, "", `Version: ${process.env.PREVIEW_VERSION}`, `Commit: ${process.env.HEAD_SHA.slice(0, 7)}`, @@ -137,10 +254,10 @@ jobs: `xattr -d com.apple.quarantine ~/Downloads/${process.env.DMG_NAME}`, "```", "", - "The download requires GitHub access and expires after 7 days.", + "No GitHub sign-in is needed. The download stays available until this PR closes or the preview label is removed.", ].join("\n"); - const { data: comments } = await github.rest.issues.listComments({ + const comments = await github.paginate(github.rest.issues.listComments, { owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.pull_request.number, @@ -163,3 +280,82 @@ jobs: body, }); } + + # The way out: closing the PR or removing the label deletes its DMG from the + # rolling release and updates the PR comment to say so. + cleanup: + name: Remove preview download + if: >- + github.event.pull_request.head.repo.full_name == github.repository && + ((github.event.action == 'closed' && contains(github.event.pull_request.labels.*.name, 'preview:mac')) || + (github.event.action == 'unlabeled' && github.event.label.name == 'preview:mac')) + runs-on: blacksmith-8vcpu-ubuntu-2404 + timeout-minutes: 10 + permissions: + contents: write + pull-requests: write + steps: + - id: delete + name: Delete this PR's preview assets + shell: bash + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + set -euo pipefail + + tag="desktop-preview" + + # A stale cleanup must not delete a download that became valid + # again. If the PR is open and labeled once more, the next publish + # owns this PR's assets and replaces them itself. + if [[ "$(gh pr view "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" \ + --json state,labels \ + --jq '.state + " " + (.labels | map(.name) | contains(["preview:mac"]) | tostring)')" == "OPEN true" ]]; then + echo "PR is open and labeled again. Skipping cleanup." + echo "removed=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + echo "removed=true" >> "$GITHUB_OUTPUT" + + if ! gh release view "$tag" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then + echo "No preview release exists. Nothing to clean up." + exit 0 + fi + + gh release view "$tag" --repo "$GITHUB_REPOSITORY" --json assets --jq '.assets[].name' \ + | { grep -F -- "-pr.${PR_NUMBER}." || true; } \ + | while read -r asset; do + gh release delete-asset "$tag" "$asset" --repo "$GITHUB_REPOSITORY" --yes \ + || echo "Asset $asset was already removed by a concurrent run." + done + + - name: Mark the preview comment as removed + if: steps.delete.outputs.removed == 'true' + uses: actions/github-script@v8 + with: + script: | + const marker = ""; + const comments = await github.paginate(github.rest.issues.listComments, { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + per_page: 100, + }); + const existing = comments.find((comment) => comment.body?.includes(marker)); + if (!existing) { + return; + } + + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body: [ + marker, + "### macOS preview", + "", + "The preview download was removed because this PR closed or the preview label was removed.", + ].join("\n"), + }); diff --git a/apps/mobile/src/features/threads/threadListV2.test.ts b/apps/mobile/src/features/threads/threadListV2.test.ts index 69ca7775e..9188b933b 100644 --- a/apps/mobile/src/features/threads/threadListV2.test.ts +++ b/apps/mobile/src/features/threads/threadListV2.test.ts @@ -304,6 +304,19 @@ describe("sortThreadsForListV2", () => { ]); expect(sorted.map((thread) => thread.id)).toEqual(["newest", "middle", "oldest"]); }); + + it("surfaces an un-settled thread at the top via its re-entry stamp", () => { + const sorted = sortThreadsForListV2([ + { + id: "old-unsettled", + createdAt: "2026-06-01T08:00:00.000Z", + unsettledAt: "2026-06-01T13:00:00.000Z", + }, + { id: "newest", createdAt: "2026-06-01T12:00:00.000Z" }, + { id: "middle", createdAt: "2026-06-01T10:00:00.000Z" }, + ]); + expect(sorted.map((thread) => thread.id)).toEqual(["old-unsettled", "newest", "middle"]); + }); }); describe("buildThreadListV2Items", () => { diff --git a/apps/mobile/src/features/threads/threadListV2.ts b/apps/mobile/src/features/threads/threadListV2.ts index c5998e253..be3343a21 100644 --- a/apps/mobile/src/features/threads/threadListV2.ts +++ b/apps/mobile/src/features/threads/threadListV2.ts @@ -12,7 +12,10 @@ import type { } from "@t3tools/client-runtime/state/thread-settled"; import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell"; import { threadSearchMatchKey } from "@t3tools/client-runtime/state/thread-search"; -import { sortPinnedThreadsByOrderKey } from "@t3tools/client-runtime/state/thread-sort"; +import { + activeThreadAnchorTimestampMs, + sortPinnedThreadsByOrderKey, +} from "@t3tools/client-runtime/state/thread-sort"; import type { EnvironmentId, ProjectId, ThreadLinkedPullRequest } from "@t3tools/contracts"; import type { PendingNewTask } from "../../state/use-pending-new-tasks"; @@ -191,19 +194,25 @@ function firstValidTimestampMs(...candidates: ReadonlyArray( - threads: readonly T[], -): T[] { +export function sortThreadsForListV2< + T extends { + readonly id: string; + readonly createdAt: string; + readonly unsettledAt?: string | null | undefined; + }, +>(threads: readonly T[]): T[] { // .sort() on a copy, not .toSorted(): Hermes doesn't ship the ES2023 // change-by-copy array methods. return [...threads].sort( (left, right) => - parseTimestampMs(right.createdAt) - parseTimestampMs(left.createdAt) || + activeThreadAnchorTimestampMs(right) - activeThreadAnchorTimestampMs(left) || left.id.localeCompare(right.id), ); } diff --git a/apps/server/src/orchestration/Layers/ProjectionPipeline.test.ts b/apps/server/src/orchestration/Layers/ProjectionPipeline.test.ts index cd95293aa..b15d2f679 100644 --- a/apps/server/src/orchestration/Layers/ProjectionPipeline.test.ts +++ b/apps/server/src/orchestration/Layers/ProjectionPipeline.test.ts @@ -269,15 +269,17 @@ it.layer(BaseTestLayer)("OrchestrationProjectionPipeline", (it) => { const settledRows = yield* sql<{ readonly settledOverride: string | null; readonly settledAt: string | null; + readonly unsettledAt: string | null; }>` SELECT settled_override AS "settledOverride", - settled_at AS "settledAt" + settled_at AS "settledAt", + unsettled_at AS "unsettledAt" FROM projection_threads WHERE thread_id = 'thread-1' `; assert.deepEqual(settledRows, [ - { settledOverride: "settled", settledAt: "2026-01-01T00:00:01.000Z" }, + { settledOverride: "settled", settledAt: "2026-01-01T00:00:01.000Z", unsettledAt: null }, ]); yield* eventStore.append({ @@ -301,14 +303,24 @@ it.layer(BaseTestLayer)("OrchestrationProjectionPipeline", (it) => { const unsettledRows = yield* sql<{ readonly settledOverride: string | null; readonly settledAt: string | null; + readonly unsettledAt: string | null; }>` SELECT settled_override AS "settledOverride", - settled_at AS "settledAt" + settled_at AS "settledAt", + unsettled_at AS "unsettledAt" FROM projection_threads WHERE thread_id = 'thread-1' `; - assert.deepEqual(unsettledRows, [{ settledOverride: "active", settledAt: null }]); + // The un-settle stamps the active-list re-entry time so clients can + // surface the thread at the top of the list. + assert.deepEqual(unsettledRows, [ + { + settledOverride: "active", + settledAt: null, + unsettledAt: "2026-01-01T00:00:02.000Z", + }, + ]); }), ); }); diff --git a/apps/server/src/orchestration/Layers/ProjectionPipeline.ts b/apps/server/src/orchestration/Layers/ProjectionPipeline.ts index e07eb4d38..15a4c77d1 100644 --- a/apps/server/src/orchestration/Layers/ProjectionPipeline.ts +++ b/apps/server/src/orchestration/Layers/ProjectionPipeline.ts @@ -624,6 +624,7 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti archivedAt: null, settledOverride: null, settledAt: null, + unsettledAt: null, snoozedUntil: null, snoozedAt: null, pinnedAt: null, @@ -682,6 +683,7 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti ...existingRow.value, settledOverride: "settled", settledAt: event.payload.settledAt, + unsettledAt: null, updatedAt: event.payload.updatedAt, }); return; @@ -698,6 +700,13 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti ...existingRow.value, settledOverride: event.payload.reason === "user" ? "active" : null, settledAt: null, + // Re-entry stamp for active-list ordering. A thread already pinned + // active keeps its stamp: the activity reset that clears the pin + // is not a re-entry and must not reorder the list. + unsettledAt: + existingRow.value.settledOverride === "active" + ? existingRow.value.unsettledAt + : event.payload.updatedAt, updatedAt: event.payload.updatedAt, }); return; diff --git a/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts b/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts index 30ba0da5b..8f5221ad3 100644 --- a/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts +++ b/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts @@ -329,6 +329,7 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => { archivedAt: null, settledOverride: null, settledAt: null, + unsettledAt: null, snoozedUntil: null, snoozedAt: null, pinnedAt: "2026-02-24T00:00:01.000Z", @@ -455,6 +456,7 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => { archivedAt: null, settledOverride: null, settledAt: null, + unsettledAt: null, snoozedUntil: null, snoozedAt: null, pinnedAt: "2026-02-24T00:00:01.000Z", diff --git a/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts b/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts index d2d8ee76c..1986a24cd 100644 --- a/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts +++ b/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts @@ -495,6 +495,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { archived_at AS "archivedAt", settled_override AS "settledOverride", settled_at AS "settledAt", + unsettled_at AS "unsettledAt", snoozed_until AS "snoozedUntil", snoozed_at AS "snoozedAt", pinned_at AS "pinnedAt", @@ -532,6 +533,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { archived_at AS "archivedAt", settled_override AS "settledOverride", settled_at AS "settledAt", + unsettled_at AS "unsettledAt", snoozed_until AS "snoozedUntil", snoozed_at AS "snoozedAt", pinned_at AS "pinnedAt", @@ -571,6 +573,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { archived_at AS "archivedAt", settled_override AS "settledOverride", settled_at AS "settledAt", + unsettled_at AS "unsettledAt", snoozed_until AS "snoozedUntil", snoozed_at AS "snoozedAt", pinned_at AS "pinnedAt", @@ -1018,6 +1021,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { archived_at AS "archivedAt", settled_override AS "settledOverride", settled_at AS "settledAt", + unsettled_at AS "unsettledAt", snoozed_until AS "snoozedUntil", snoozed_at AS "snoozedAt", pinned_at AS "pinnedAt", @@ -1780,6 +1784,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { archivedAt: row.archivedAt, settledOverride: row.settledOverride, settledAt: row.settledAt, + unsettledAt: row.unsettledAt, snoozedUntil: row.snoozedUntil, snoozedAt: row.snoozedAt, pinnedAt: row.pinnedAt, @@ -1992,6 +1997,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { archivedAt: row.archivedAt, settledOverride: row.settledOverride, settledAt: row.settledAt, + unsettledAt: row.unsettledAt, snoozedUntil: row.snoozedUntil, snoozedAt: row.snoozedAt, pinnedAt: row.pinnedAt, @@ -2133,6 +2139,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { archivedAt: row.archivedAt, settledOverride: row.settledOverride, settledAt: row.settledAt, + unsettledAt: row.unsettledAt, snoozedUntil: row.snoozedUntil, snoozedAt: row.snoozedAt, pinnedAt: row.pinnedAt, @@ -2283,6 +2290,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { archivedAt: row.archivedAt, settledOverride: row.settledOverride, settledAt: row.settledAt, + unsettledAt: row.unsettledAt, snoozedUntil: row.snoozedUntil, snoozedAt: row.snoozedAt, pinnedAt: row.pinnedAt, @@ -2571,6 +2579,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { archivedAt: threadRow.value.archivedAt, settledOverride: threadRow.value.settledOverride, settledAt: threadRow.value.settledAt, + unsettledAt: threadRow.value.unsettledAt, snoozedUntil: threadRow.value.snoozedUntil, snoozedAt: threadRow.value.snoozedAt, pinnedAt: threadRow.value.pinnedAt, @@ -2721,6 +2730,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { archivedAt: threadRow.value.archivedAt, settledOverride: threadRow.value.settledOverride, settledAt: threadRow.value.settledAt, + unsettledAt: threadRow.value.unsettledAt, snoozedUntil: threadRow.value.snoozedUntil, snoozedAt: threadRow.value.snoozedAt, pinnedAt: threadRow.value.pinnedAt, diff --git a/apps/server/src/orchestration/decider.settled.test.ts b/apps/server/src/orchestration/decider.settled.test.ts index 29b81a12c..c9d119201 100644 --- a/apps/server/src/orchestration/decider.settled.test.ts +++ b/apps/server/src/orchestration/decider.settled.test.ts @@ -5,6 +5,7 @@ import { ProjectId, ProviderInstanceId, ThreadId, + type OrchestrationEvent, type OrchestrationReadModel, type OrchestrationSession, type OrchestrationThread, @@ -14,6 +15,7 @@ import { expect, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; import { decideOrchestrationCommand } from "./decider.ts"; +import { projectEvent } from "./projector.ts"; const NOW = "2026-01-01T00:00:00.000Z"; const SETTLED_AT = "2025-12-30T00:00:00.000Z"; @@ -429,6 +431,42 @@ it.layer(NodeServices.layer)("settled thread decider", (it) => { }), ); + // Command-to-projection: an accepted un-settle must land as the re-entry + // stamp clients sort by (max of createdAt and unsettledAt, see + // activeThreadAnchorTimestampMs in client-runtime), so the thread surfaces + // above threads created after it. The projector tests feed events directly; + // this one proves the decider actually emits what they consume. + it.effect("an accepted un-settle re-anchors the thread for the active list", () => + Effect.gen(function* () { + const readModel = makeReadModel("settled"); + const result = yield* decideOrchestrationCommand({ + command: { + type: "thread.unsettle", + commandId: CommandId.make("cmd-unsettle-anchor"), + threadId: ThreadId.make("thread-1"), + reason: "user", + }, + readModel, + }); + const events = Array.isArray(result) ? result : [result]; + const unsettled = events[0]!; + expect(unsettled.type).toBe("thread.unsettled"); + + const projected = yield* projectEvent(readModel, { + ...unsettled, + sequence: readModel.snapshotSequence + 1, + } as OrchestrationEvent); + const thread = projected.threads[0]!; + expect(thread.settledOverride).toBe("active"); + // The stamp is the decider's accept time: every thread created before + // the un-settle anchors below it. + expect(thread.unsettledAt).toBe(unsettled.occurredAt); + if (unsettled.type === "thread.unsettled") { + expect(thread.unsettledAt).toBe(unsettled.payload.updatedAt); + } + }), + ); + it.effect("prepends activity unsets for turn starts and live session updates", () => Effect.gen(function* () { const turnResult = yield* decideOrchestrationCommand({ diff --git a/apps/server/src/orchestration/projector.settled.test.ts b/apps/server/src/orchestration/projector.settled.test.ts index 2070c4441..7c9395e6d 100644 --- a/apps/server/src/orchestration/projector.settled.test.ts +++ b/apps/server/src/orchestration/projector.settled.test.ts @@ -62,27 +62,62 @@ it.effect("projects settled lifecycle events", () => ); expect(settled.threads[0]?.settledOverride).toBe("settled"); expect(settled.threads[0]?.settledAt).toBe(now); + expect(settled.threads[0]?.unsettledAt).toBeNull(); + const unsettleAt = "2026-01-02T00:00:00.000Z"; const userUnsettled = yield* projectEvent( settled, makeEvent({ sequence: 3, type: "thread.unsettled", - payload: { threadId: ThreadId.make("thread-1"), reason: "user", updatedAt: now }, + payload: { threadId: ThreadId.make("thread-1"), reason: "user", updatedAt: unsettleAt }, }), ); expect(userUnsettled.threads[0]?.settledOverride).toBe("active"); expect(userUnsettled.threads[0]?.settledAt).toBeNull(); + expect(userUnsettled.threads[0]?.unsettledAt).toBe(unsettleAt); + // Clearing the keep-active pin on activity is not a re-entry: the thread + // is already in the active list, so the stamp must not move it. + const activityAt = "2026-01-03T00:00:00.000Z"; const activityUnsettled = yield* projectEvent( userUnsettled, makeEvent({ sequence: 4, type: "thread.unsettled", - payload: { threadId: ThreadId.make("thread-1"), reason: "activity", updatedAt: now }, + payload: { threadId: ThreadId.make("thread-1"), reason: "activity", updatedAt: activityAt }, }), ); expect(activityUnsettled.threads[0]?.settledOverride).toBeNull(); expect(activityUnsettled.threads[0]?.settledAt).toBeNull(); + expect(activityUnsettled.threads[0]?.unsettledAt).toBe(unsettleAt); + + const resettledAt = "2026-01-04T00:00:00.000Z"; + const resettled = yield* projectEvent( + activityUnsettled, + makeEvent({ + sequence: 5, + type: "thread.settled", + payload: { + threadId: ThreadId.make("thread-1"), + settledAt: resettledAt, + updatedAt: resettledAt, + }, + }), + ); + expect(resettled.threads[0]?.unsettledAt).toBeNull(); + + // Waking a settled thread on activity IS a re-entry and stamps. + const wakeAt = "2026-01-05T00:00:00.000Z"; + const woke = yield* projectEvent( + resettled, + makeEvent({ + sequence: 6, + type: "thread.unsettled", + payload: { threadId: ThreadId.make("thread-1"), reason: "activity", updatedAt: wakeAt }, + }), + ); + expect(woke.threads[0]?.settledOverride).toBeNull(); + expect(woke.threads[0]?.unsettledAt).toBe(wakeAt); }), ); diff --git a/apps/server/src/orchestration/projector.test.ts b/apps/server/src/orchestration/projector.test.ts index 86cde85e4..53f1b666a 100644 --- a/apps/server/src/orchestration/projector.test.ts +++ b/apps/server/src/orchestration/projector.test.ts @@ -91,6 +91,7 @@ describe("orchestration projector", () => { archivedAt: null, settledOverride: null, settledAt: null, + unsettledAt: null, snoozedUntil: null, snoozedAt: null, deletedAt: null, diff --git a/apps/server/src/orchestration/projector.ts b/apps/server/src/orchestration/projector.ts index 370584bfc..33e2c7a21 100644 --- a/apps/server/src/orchestration/projector.ts +++ b/apps/server/src/orchestration/projector.ts @@ -305,6 +305,7 @@ export function projectEvent( archivedAt: null, settledOverride: null, settledAt: null, + unsettledAt: null, snoozedUntil: null, snoozedAt: null, deletedAt: null, @@ -367,6 +368,7 @@ export function projectEvent( threads: updateThread(nextBase.threads, payload.threadId, { settledOverride: "settled", settledAt: payload.settledAt, + unsettledAt: null, updatedAt: payload.updatedAt, }), })), @@ -374,14 +376,24 @@ export function projectEvent( case "thread.unsettled": return decodeForEvent(ThreadUnsettledPayload, event.payload, event.type, "payload").pipe( - Effect.map((payload) => ({ - ...nextBase, - threads: updateThread(nextBase.threads, payload.threadId, { - settledOverride: payload.reason === "user" ? "active" : null, - settledAt: null, - updatedAt: payload.updatedAt, - }), - })), + Effect.map((payload) => { + const existing = nextBase.threads.find((thread) => thread.id === payload.threadId); + return { + ...nextBase, + threads: updateThread(nextBase.threads, payload.threadId, { + settledOverride: payload.reason === "user" ? "active" : null, + settledAt: null, + // Re-entry stamp for active-list ordering. A thread already + // pinned active keeps its stamp: the activity reset that clears + // the pin is not a re-entry and must not reorder the list. + unsettledAt: + existing?.settledOverride === "active" + ? (existing.unsettledAt ?? null) + : payload.updatedAt, + updatedAt: payload.updatedAt, + }), + }; + }), ); case "thread.snoozed": diff --git a/apps/server/src/persistence/Layers/ProjectionRepositories.test.ts b/apps/server/src/persistence/Layers/ProjectionRepositories.test.ts index 19f95a9ec..ba1c1d0df 100644 --- a/apps/server/src/persistence/Layers/ProjectionRepositories.test.ts +++ b/apps/server/src/persistence/Layers/ProjectionRepositories.test.ts @@ -94,6 +94,7 @@ projectionRepositoriesLayer("Projection repositories", (it) => { archivedAt: null, settledOverride: null, settledAt: null, + unsettledAt: null, snoozedUntil: null, snoozedAt: null, pinnedAt: null, @@ -158,6 +159,7 @@ projectionRepositoriesLayer("Projection repositories", (it) => { archivedAt: null, settledOverride: "settled", settledAt: "2026-03-25T00:00:00.000Z", + unsettledAt: null, snoozedUntil: "2026-03-26T09:00:00.000Z", snoozedAt: "2026-03-25T00:00:00.000Z", pinnedAt: "2026-03-25T00:00:00.000Z", @@ -188,6 +190,7 @@ projectionRepositoriesLayer("Projection repositories", (it) => { ...row, settledOverride: "active", settledAt: null, + unsettledAt: "2026-03-26T00:00:00.000Z", snoozedUntil: null, snoozedAt: null, pinnedAt: null, @@ -198,6 +201,7 @@ projectionRepositoriesLayer("Projection repositories", (it) => { const updated = Option.getOrNull(repersisted); assert.strictEqual(updated?.settledOverride, "active"); assert.strictEqual(updated?.settledAt, null); + assert.strictEqual(updated?.unsettledAt, "2026-03-26T00:00:00.000Z"); assert.strictEqual(updated?.snoozedUntil, null); assert.strictEqual(updated?.snoozedAt, null); assert.strictEqual(updated?.pinnedAt, null); @@ -233,6 +237,7 @@ projectionRepositoriesLayer("Projection repositories", (it) => { archivedAt: null, settledOverride: null, settledAt: null, + unsettledAt: null, snoozedUntil: null, snoozedAt: null, pinnedAt: null, diff --git a/apps/server/src/persistence/Layers/ProjectionThreads.ts b/apps/server/src/persistence/Layers/ProjectionThreads.ts index 7f8cace22..a3d6a3308 100644 --- a/apps/server/src/persistence/Layers/ProjectionThreads.ts +++ b/apps/server/src/persistence/Layers/ProjectionThreads.ts @@ -50,6 +50,7 @@ const makeProjectionThreadRepository = Effect.gen(function* () { archived_at, settled_override, settled_at, + unsettled_at, snoozed_until, snoozed_at, pinned_at, @@ -78,6 +79,7 @@ const makeProjectionThreadRepository = Effect.gen(function* () { ${row.archivedAt}, ${row.settledOverride}, ${row.settledAt}, + ${row.unsettledAt}, ${row.snoozedUntil}, ${row.snoozedAt}, ${row.pinnedAt}, @@ -106,6 +108,7 @@ const makeProjectionThreadRepository = Effect.gen(function* () { archived_at = excluded.archived_at, settled_override = excluded.settled_override, settled_at = excluded.settled_at, + unsettled_at = excluded.unsettled_at, snoozed_until = excluded.snoozed_until, snoozed_at = excluded.snoozed_at, pinned_at = excluded.pinned_at, @@ -141,6 +144,7 @@ const makeProjectionThreadRepository = Effect.gen(function* () { archived_at AS "archivedAt", settled_override AS "settledOverride", settled_at AS "settledAt", + unsettled_at AS "unsettledAt", snoozed_until AS "snoozedUntil", snoozed_at AS "snoozedAt", pinned_at AS "pinnedAt", @@ -178,6 +182,7 @@ const makeProjectionThreadRepository = Effect.gen(function* () { archived_at AS "archivedAt", settled_override AS "settledOverride", settled_at AS "settledAt", + unsettled_at AS "unsettledAt", snoozed_until AS "snoozedUntil", snoozed_at AS "snoozedAt", pinned_at AS "pinnedAt", diff --git a/apps/server/src/persistence/Migrations.ts b/apps/server/src/persistence/Migrations.ts index 52eedae90..8abbe87fc 100644 --- a/apps/server/src/persistence/Migrations.ts +++ b/apps/server/src/persistence/Migrations.ts @@ -55,6 +55,7 @@ import Migration0039 from "./Migrations/039_ProjectionProjectsDefaultThreadEnvMo import Migration0040 from "./Migrations/040_ProjectionProjectFaviconPath.ts"; import Migration0041 from "./Migrations/041_AuthSessionClientConnection.ts"; import Migration0042 from "./Migrations/042_ProjectionThreadLinkedPullRequest.ts"; +import Migration0043 from "./Migrations/043_ProjectionThreadsUnsettledAt.ts"; /** * Migration loader with all migrations defined inline. @@ -109,6 +110,7 @@ export const migrationEntries = [ [40, "ProjectionProjectFaviconPath", Migration0040], [41, "AuthSessionClientConnection", Migration0041], [42, "ProjectionThreadLinkedPullRequest", Migration0042], + [43, "ProjectionThreadsUnsettledAt", Migration0043], ] as const; export const migrationManifest = migrationEntries.map(([id, name]) => [id, name] as const); diff --git a/apps/server/src/persistence/Migrations/043_ProjectionThreadsUnsettledAt.ts b/apps/server/src/persistence/Migrations/043_ProjectionThreadsUnsettledAt.ts new file mode 100644 index 000000000..981d3c78f --- /dev/null +++ b/apps/server/src/persistence/Migrations/043_ProjectionThreadsUnsettledAt.ts @@ -0,0 +1,16 @@ +import * as Effect from "effect/Effect"; +import * as SqlClient from "effect/unstable/sql/SqlClient"; + +export default Effect.gen(function* () { + const sql = yield* SqlClient.SqlClient; + const columns = yield* sql<{ readonly name: string }>` + PRAGMA table_info(projection_threads) + `; + + if (!columns.some((column) => column.name === "unsettled_at")) { + yield* sql` + ALTER TABLE projection_threads + ADD COLUMN unsettled_at TEXT + `; + } +}); diff --git a/apps/server/src/persistence/Services/ProjectionThreads.ts b/apps/server/src/persistence/Services/ProjectionThreads.ts index e82c750e7..59cf3297b 100644 --- a/apps/server/src/persistence/Services/ProjectionThreads.ts +++ b/apps/server/src/persistence/Services/ProjectionThreads.ts @@ -42,6 +42,7 @@ export const ProjectionThread = Schema.Struct({ archivedAt: Schema.NullOr(IsoDateTime), settledOverride: Schema.NullOr(Schema.Literals(["settled", "active"])), settledAt: Schema.NullOr(IsoDateTime), + unsettledAt: Schema.NullOr(IsoDateTime), snoozedUntil: Schema.NullOr(IsoDateTime), snoozedAt: Schema.NullOr(IsoDateTime), pinnedAt: Schema.NullOr(IsoDateTime), diff --git a/apps/web/src/components/Sidebar.logic.test.ts b/apps/web/src/components/Sidebar.logic.test.ts index 402c55d31..1bb1f44e5 100644 --- a/apps/web/src/components/Sidebar.logic.test.ts +++ b/apps/web/src/components/Sidebar.logic.test.ts @@ -821,6 +821,33 @@ describe("sortThreadsForSidebar", () => { expect(sorted.map((thread) => thread.id)).toEqual(["a", "b"]); }); + + it("surfaces an un-settled thread at the top via its re-entry stamp", () => { + const sorted = sortThreadsForSidebar([ + { + id: "old-unsettled", + createdAt: "2026-03-09T08:00:00.000Z", + unsettledAt: "2026-03-09T13:00:00.000Z", + }, + sortable({ id: "newest", createdAt: "2026-03-09T12:00:00.000Z" }), + sortable({ id: "middle", createdAt: "2026-03-09T10:00:00.000Z" }), + ]); + + expect(sorted.map((thread) => thread.id)).toEqual(["old-unsettled", "newest", "middle"]); + }); + + it("ignores a re-entry stamp older than the thread's creation", () => { + const sorted = sortThreadsForSidebar([ + { + id: "stale-stamp", + createdAt: "2026-03-09T10:00:00.000Z", + unsettledAt: "2026-03-09T09:00:00.000Z", + }, + sortable({ id: "newest", createdAt: "2026-03-09T12:00:00.000Z" }), + ]); + + expect(sorted.map((thread) => thread.id)).toEqual(["newest", "stale-stamp"]); + }); }); describe("pinOrderKeyBetween", () => { diff --git a/apps/web/src/components/Sidebar.logic.ts b/apps/web/src/components/Sidebar.logic.ts index 9225c59f7..4d87f90d0 100644 --- a/apps/web/src/components/Sidebar.logic.ts +++ b/apps/web/src/components/Sidebar.logic.ts @@ -3,6 +3,7 @@ import { defaultAnimateLayoutChanges, type AnimateLayoutChanges } from "@dnd-kit import type { ContextMenuItem } from "@t3tools/contracts"; import type { SidebarProjectSortOrder, SidebarThreadSortOrder } from "@t3tools/contracts/settings"; import { + activeThreadAnchorTimestampMs, getThreadSortTimestamp, sortThreads, toSortableTimestamp, @@ -538,16 +539,23 @@ export function firstValidTimestamp( return null; } -// Sidebar sort: static creation order, newest thread on top. Activity NEVER -// reorders the list — a row holds its position from open until settled, so -// the screen only moves at lifecycle transitions. Status (including pending -// approval) is carried by each card's edge strip, not by position. +// Sidebar sort: static order, newest anchor on top. Activity NEVER reorders +// the list — a row holds its position between lifecycle transitions, so the +// screen only moves when a thread enters or leaves the active list. The +// anchor is creation time until an un-settle re-anchors it (see +// activeThreadAnchorTimestampMs), so an un-settled thread surfaces at the +// top instead of sinking back to its creation-order slot. Status (including +// pending approval) is carried by each card's edge strip, not by position. export function sortThreadsForSidebar< - T extends { readonly id: string; readonly createdAt: string }, + T extends { + readonly id: string; + readonly createdAt: string; + readonly unsettledAt?: string | null | undefined; + }, >(threads: readonly T[]): T[] { return [...threads].toSorted( (left, right) => - parseTimestampMs(right.createdAt) - parseTimestampMs(left.createdAt) || + activeThreadAnchorTimestampMs(right) - activeThreadAnchorTimestampMs(left) || left.id.localeCompare(right.id), ); } diff --git a/apps/web/src/lib/threadSort.ts b/apps/web/src/lib/threadSort.ts index ac3dea3ac..53438305c 100644 --- a/apps/web/src/lib/threadSort.ts +++ b/apps/web/src/lib/threadSort.ts @@ -1,4 +1,5 @@ export { + activeThreadAnchorTimestampMs, getLatestThreadForProject, getThreadSortTimestamp, sortThreads, diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx index c8b08367d..a7e2b78f1 100644 --- a/apps/web/src/main.tsx +++ b/apps/web/src/main.tsx @@ -31,11 +31,6 @@ if (isElectron) { const clerkPublishableKey = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY as string | undefined; -// First Clerk UI build containing https://github.com/clerk/javascript/pull/9500. -const electronClerkUI = { - __internal_clerkUIVersion: "1.30.5-canary.v20260819050620", -}; - const app = ; ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( @@ -43,7 +38,6 @@ ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( {clerkPublishableKey && hasCloudPublicConfig() ? ( isElectron ? ( ( threads: readonly T[], sortOrder: SidebarThreadSortOrder, diff --git a/packages/contracts/src/orchestration.ts b/packages/contracts/src/orchestration.ts index 34aa3fc22..b025ac76e 100644 --- a/packages/contracts/src/orchestration.ts +++ b/packages/contracts/src/orchestration.ts @@ -470,6 +470,11 @@ export const OrchestrationThread = Schema.Struct({ Schema.withDecodingDefault(Effect.succeed(null)), ), settledAt: Schema.NullOr(IsoDateTime).pipe(Schema.withDecodingDefault(Effect.succeed(null))), + // When the thread last re-entered the active list (any thread.unsettled). + // Anchors the active-list sort so an unsettled thread surfaces at the top + // instead of sinking back to its creation-order slot. Cleared on settle. + // Optional so payloads from pre-stamp servers still decode. + unsettledAt: Schema.optional(Schema.NullOr(IsoDateTime)), // Snooze is an overlay on the active lifecycle, not a fourth destination: // a snoozed thread stays "active" in the model and is only suppressed from // the inbox until snoozedUntil passes (or the thread raises its hand). @@ -545,6 +550,8 @@ export const OrchestrationThreadShell = Schema.Struct({ Schema.withDecodingDefault(Effect.succeed(null)), ), settledAt: Schema.NullOr(IsoDateTime).pipe(Schema.withDecodingDefault(Effect.succeed(null))), + // See OrchestrationThread.unsettledAt: last re-entry into the active list. + unsettledAt: Schema.optional(Schema.NullOr(IsoDateTime)), snoozedUntil: Schema.optional(Schema.NullOr(IsoDateTime)), snoozedAt: Schema.optional(Schema.NullOr(IsoDateTime)), pinnedAt: Schema.optional(Schema.NullOr(IsoDateTime)), diff --git a/packages/effect-codex-app-server/scripts/generate.ts b/packages/effect-codex-app-server/scripts/generate.ts index 9f23a1445..44de61d28 100644 --- a/packages/effect-codex-app-server/scripts/generate.ts +++ b/packages/effect-codex-app-server/scripts/generate.ts @@ -145,6 +145,33 @@ const ManualSchemas: Record = { }, }; +// Codex 0.150 added these multi-agent values before our next full protocol +// refresh. Keep every generated response namespace compatible with them. +const Codex0150DefinitionSchemas: Record = { + CollabAgentTool: { + type: "string", + enum: [ + "spawnAgent", + "sendInput", + "resumeAgent", + "wait", + "closeAgent", + "sendMessage", + "followupTask", + "interruptAgent", + "listAgents", + ], + }, + CollabAgentToolCallStatus: { + type: "string", + enum: ["inProgress", "completed", "failed", "interrupted"], + }, + SubAgentActivityKind: { + type: "string", + enum: ["started", "interacted", "interrupted", "completed"], + }, +}; + const getGeneratedPaths = Effect.fn("getGeneratedPaths")(function* () { const path = yield* Path.Path; const generatedDir = path.join(import.meta.dirname, "..", "src", "_generated"); @@ -556,10 +583,12 @@ const generateFiles = Effect.fn("generateFiles")(function* () { ); for (const [definitionName, definitionSchema] of Object.entries(parsed.definitions ?? {})) { + const compatibleDefinitionSchema = + Codex0150DefinitionSchemas[definitionName] ?? definitionSchema; aggregateSchemas[localDefinitionNames.get(definitionName)!] = stripNullDefaults( normalizeNullableTypes( rewriteExternalRefs( - definitionSchema, + compatibleDefinitionSchema, localDefinitionNames, file.namespace, exportNameByQualifiedName, diff --git a/packages/effect-codex-app-server/src/_generated/schema.gen.ts b/packages/effect-codex-app-server/src/_generated/schema.gen.ts index d826df60f..6b200a420 100644 --- a/packages/effect-codex-app-server/src/_generated/schema.gen.ts +++ b/packages/effect-codex-app-server/src/_generated/schema.gen.ts @@ -2616,11 +2616,16 @@ export const ServerNotification__SpendControlLimitSnapshot = Schema.Struct({ used: Schema.String, }); -export type ServerNotification__SubAgentActivityKind = "started" | "interacted" | "interrupted"; +export type ServerNotification__SubAgentActivityKind = + | "started" + | "interacted" + | "interrupted" + | "completed"; export const ServerNotification__SubAgentActivityKind = Schema.Literals([ "started", "interacted", "interrupted", + "completed", ]); export type ServerNotification__TerminalInteractionNotification = { @@ -4710,11 +4715,13 @@ export const V2ItemCompletedNotification__ReasoningEffort = Schema.String.annota export type V2ItemCompletedNotification__SubAgentActivityKind = | "started" | "interacted" - | "interrupted"; + | "interrupted" + | "completed"; export const V2ItemCompletedNotification__SubAgentActivityKind = Schema.Literals([ "started", "interacted", "interrupted", + "completed", ]); export type V2ItemCompletedNotification__TextElement = { @@ -5115,11 +5122,13 @@ export const V2ItemStartedNotification__ReasoningEffort = Schema.String.annotate export type V2ItemStartedNotification__SubAgentActivityKind = | "started" | "interacted" - | "interrupted"; + | "interrupted" + | "completed"; export const V2ItemStartedNotification__SubAgentActivityKind = Schema.Literals([ "started", "interacted", "interrupted", + "completed", ]); export type V2ItemStartedNotification__TextElement = { @@ -6284,11 +6293,16 @@ export const V2ReviewStartResponse__ReasoningEffort = Schema.String.annotate({ description: "A non-empty reasoning effort value advertised by the model.", }).check(Schema.isMinLength(1)); -export type V2ReviewStartResponse__SubAgentActivityKind = "started" | "interacted" | "interrupted"; +export type V2ReviewStartResponse__SubAgentActivityKind = + | "started" + | "interacted" + | "interrupted" + | "completed"; export const V2ReviewStartResponse__SubAgentActivityKind = Schema.Literals([ "started", "interacted", "interrupted", + "completed", ]); export type V2ReviewStartResponse__TextElement = { @@ -6720,11 +6734,16 @@ export const V2ThreadForkResponse__ReasoningEffort = Schema.String.annotate({ description: "A non-empty reasoning effort value advertised by the model.", }).check(Schema.isMinLength(1)); -export type V2ThreadForkResponse__SubAgentActivityKind = "started" | "interacted" | "interrupted"; +export type V2ThreadForkResponse__SubAgentActivityKind = + | "started" + | "interacted" + | "interrupted" + | "completed"; export const V2ThreadForkResponse__SubAgentActivityKind = Schema.Literals([ "started", "interacted", "interrupted", + "completed", ]); export type V2ThreadForkResponse__TextElement = { @@ -7119,11 +7138,16 @@ export const V2ThreadListResponse__ReasoningEffort = Schema.String.annotate({ description: "A non-empty reasoning effort value advertised by the model.", }).check(Schema.isMinLength(1)); -export type V2ThreadListResponse__SubAgentActivityKind = "started" | "interacted" | "interrupted"; +export type V2ThreadListResponse__SubAgentActivityKind = + | "started" + | "interacted" + | "interrupted" + | "completed"; export const V2ThreadListResponse__SubAgentActivityKind = Schema.Literals([ "started", "interacted", "interrupted", + "completed", ]); export type V2ThreadListResponse__TextElement = { @@ -7463,11 +7487,13 @@ export const V2ThreadMetadataUpdateResponse__ReasoningEffort = Schema.String.ann export type V2ThreadMetadataUpdateResponse__SubAgentActivityKind = | "started" | "interacted" - | "interrupted"; + | "interrupted" + | "completed"; export const V2ThreadMetadataUpdateResponse__SubAgentActivityKind = Schema.Literals([ "started", "interacted", "interrupted", + "completed", ]); export type V2ThreadMetadataUpdateResponse__TextElement = { @@ -7760,11 +7786,16 @@ export const V2ThreadReadResponse__ReasoningEffort = Schema.String.annotate({ description: "A non-empty reasoning effort value advertised by the model.", }).check(Schema.isMinLength(1)); -export type V2ThreadReadResponse__SubAgentActivityKind = "started" | "interacted" | "interrupted"; +export type V2ThreadReadResponse__SubAgentActivityKind = + | "started" + | "interacted" + | "interrupted" + | "completed"; export const V2ThreadReadResponse__SubAgentActivityKind = Schema.Literals([ "started", "interacted", "interrupted", + "completed", ]); export type V2ThreadReadResponse__TextElement = { @@ -8342,11 +8373,16 @@ export const V2ThreadResumeResponse__ReasoningEffort = Schema.String.annotate({ description: "A non-empty reasoning effort value advertised by the model.", }).check(Schema.isMinLength(1)); -export type V2ThreadResumeResponse__SubAgentActivityKind = "started" | "interacted" | "interrupted"; +export type V2ThreadResumeResponse__SubAgentActivityKind = + | "started" + | "interacted" + | "interrupted" + | "completed"; export const V2ThreadResumeResponse__SubAgentActivityKind = Schema.Literals([ "started", "interacted", "interrupted", + "completed", ]); export type V2ThreadResumeResponse__TextElement = { @@ -8643,11 +8679,13 @@ export const V2ThreadRollbackResponse__ReasoningEffort = Schema.String.annotate( export type V2ThreadRollbackResponse__SubAgentActivityKind = | "started" | "interacted" - | "interrupted"; + | "interrupted" + | "completed"; export const V2ThreadRollbackResponse__SubAgentActivityKind = Schema.Literals([ "started", "interacted", "interrupted", + "completed", ]); export type V2ThreadRollbackResponse__TextElement = { @@ -9051,11 +9089,13 @@ export const V2ThreadStartedNotification__ReasoningEffort = Schema.String.annota export type V2ThreadStartedNotification__SubAgentActivityKind = | "started" | "interacted" - | "interrupted"; + | "interrupted" + | "completed"; export const V2ThreadStartedNotification__SubAgentActivityKind = Schema.Literals([ "started", "interacted", "interrupted", + "completed", ]); export type V2ThreadStartedNotification__TextElement = { @@ -9458,11 +9498,16 @@ export const V2ThreadStartResponse__ReasoningEffort = Schema.String.annotate({ description: "A non-empty reasoning effort value advertised by the model.", }).check(Schema.isMinLength(1)); -export type V2ThreadStartResponse__SubAgentActivityKind = "started" | "interacted" | "interrupted"; +export type V2ThreadStartResponse__SubAgentActivityKind = + | "started" + | "interacted" + | "interrupted" + | "completed"; export const V2ThreadStartResponse__SubAgentActivityKind = Schema.Literals([ "started", "interacted", "interrupted", + "completed", ]); export type V2ThreadStartResponse__TextElement = { @@ -9791,11 +9836,13 @@ export const V2ThreadUnarchiveResponse__ReasoningEffort = Schema.String.annotate export type V2ThreadUnarchiveResponse__SubAgentActivityKind = | "started" | "interacted" - | "interrupted"; + | "interrupted" + | "completed"; export const V2ThreadUnarchiveResponse__SubAgentActivityKind = Schema.Literals([ "started", "interacted", "interrupted", + "completed", ]); export type V2ThreadUnarchiveResponse__TextElement = { @@ -10095,11 +10142,13 @@ export const V2TurnCompletedNotification__ReasoningEffort = Schema.String.annota export type V2TurnCompletedNotification__SubAgentActivityKind = | "started" | "interacted" - | "interrupted"; + | "interrupted" + | "completed"; export const V2TurnCompletedNotification__SubAgentActivityKind = Schema.Literals([ "started", "interacted", "interrupted", + "completed", ]); export type V2TurnCompletedNotification__TextElement = { @@ -10385,11 +10434,13 @@ export const V2TurnStartedNotification__ReasoningEffort = Schema.String.annotate export type V2TurnStartedNotification__SubAgentActivityKind = | "started" | "interacted" - | "interrupted"; + | "interrupted" + | "completed"; export const V2TurnStartedNotification__SubAgentActivityKind = Schema.Literals([ "started", "interacted", "interrupted", + "completed", ]); export type V2TurnStartedNotification__TextElement = { @@ -10761,11 +10812,16 @@ export const V2TurnStartResponse__ReasoningEffort = Schema.String.annotate({ description: "A non-empty reasoning effort value advertised by the model.", }).check(Schema.isMinLength(1)); -export type V2TurnStartResponse__SubAgentActivityKind = "started" | "interacted" | "interrupted"; +export type V2TurnStartResponse__SubAgentActivityKind = + | "started" + | "interacted" + | "interrupted" + | "completed"; export const V2TurnStartResponse__SubAgentActivityKind = Schema.Literals([ "started", "interacted", "interrupted", + "completed", ]); export type V2TurnStartResponse__TextElement = { @@ -20357,8 +20413,17 @@ export type ServerNotification__ThreadItem = readonly reasoningEffort?: ServerNotification__ReasoningEffort | null; readonly receiverThreadIds: ReadonlyArray; readonly senderThreadId: string; - readonly status: "inProgress" | "completed" | "failed"; - readonly tool: "spawnAgent" | "sendInput" | "resumeAgent" | "wait" | "closeAgent"; + readonly status: "inProgress" | "completed" | "failed" | "interrupted"; + readonly tool: + | "spawnAgent" + | "sendInput" + | "resumeAgent" + | "wait" + | "closeAgent" + | "sendMessage" + | "followupTask" + | "interruptAgent" + | "listAgents"; readonly type: "collabAgentToolCall"; } | { @@ -20580,7 +20645,7 @@ export const ServerNotification__ThreadItem = Schema.Union( senderThreadId: Schema.String.annotate({ description: "Thread ID of the agent issuing the collab request.", }), - status: Schema.Literals(["inProgress", "completed", "failed"]).annotate({ + status: Schema.Literals(["inProgress", "completed", "failed", "interrupted"]).annotate({ description: "Current status of the collab tool call.", }), tool: Schema.Literals([ @@ -20589,6 +20654,10 @@ export const ServerNotification__ThreadItem = Schema.Union( "resumeAgent", "wait", "closeAgent", + "sendMessage", + "followupTask", + "interruptAgent", + "listAgents", ]).annotate({ description: "Name of the collab tool that was invoked." }), type: Schema.Literal("collabAgentToolCall").annotate({ title: "CollabAgentToolCallThreadItemType", @@ -21440,8 +21509,17 @@ export type V2ItemCompletedNotification__ThreadItem = readonly reasoningEffort?: V2ItemCompletedNotification__ReasoningEffort | null; readonly receiverThreadIds: ReadonlyArray; readonly senderThreadId: string; - readonly status: "inProgress" | "completed" | "failed"; - readonly tool: "spawnAgent" | "sendInput" | "resumeAgent" | "wait" | "closeAgent"; + readonly status: "inProgress" | "completed" | "failed" | "interrupted"; + readonly tool: + | "spawnAgent" + | "sendInput" + | "resumeAgent" + | "wait" + | "closeAgent" + | "sendMessage" + | "followupTask" + | "interruptAgent" + | "listAgents"; readonly type: "collabAgentToolCall"; } | { @@ -21668,7 +21746,7 @@ export const V2ItemCompletedNotification__ThreadItem = Schema.Union( senderThreadId: Schema.String.annotate({ description: "Thread ID of the agent issuing the collab request.", }), - status: Schema.Literals(["inProgress", "completed", "failed"]).annotate({ + status: Schema.Literals(["inProgress", "completed", "failed", "interrupted"]).annotate({ description: "Current status of the collab tool call.", }), tool: Schema.Literals([ @@ -21677,6 +21755,10 @@ export const V2ItemCompletedNotification__ThreadItem = Schema.Union( "resumeAgent", "wait", "closeAgent", + "sendMessage", + "followupTask", + "interruptAgent", + "listAgents", ]).annotate({ description: "Name of the collab tool that was invoked." }), type: Schema.Literal("collabAgentToolCall").annotate({ title: "CollabAgentToolCallThreadItemType", @@ -21891,8 +21973,17 @@ export type V2ItemStartedNotification__ThreadItem = readonly reasoningEffort?: V2ItemStartedNotification__ReasoningEffort | null; readonly receiverThreadIds: ReadonlyArray; readonly senderThreadId: string; - readonly status: "inProgress" | "completed" | "failed"; - readonly tool: "spawnAgent" | "sendInput" | "resumeAgent" | "wait" | "closeAgent"; + readonly status: "inProgress" | "completed" | "failed" | "interrupted"; + readonly tool: + | "spawnAgent" + | "sendInput" + | "resumeAgent" + | "wait" + | "closeAgent" + | "sendMessage" + | "followupTask" + | "interruptAgent" + | "listAgents"; readonly type: "collabAgentToolCall"; } | { @@ -22119,7 +22210,7 @@ export const V2ItemStartedNotification__ThreadItem = Schema.Union( senderThreadId: Schema.String.annotate({ description: "Thread ID of the agent issuing the collab request.", }), - status: Schema.Literals(["inProgress", "completed", "failed"]).annotate({ + status: Schema.Literals(["inProgress", "completed", "failed", "interrupted"]).annotate({ description: "Current status of the collab tool call.", }), tool: Schema.Literals([ @@ -22128,6 +22219,10 @@ export const V2ItemStartedNotification__ThreadItem = Schema.Union( "resumeAgent", "wait", "closeAgent", + "sendMessage", + "followupTask", + "interruptAgent", + "listAgents", ]).annotate({ description: "Name of the collab tool that was invoked." }), type: Schema.Literal("collabAgentToolCall").annotate({ title: "CollabAgentToolCallThreadItemType", @@ -22514,8 +22609,17 @@ export type V2ReviewStartResponse__ThreadItem = readonly reasoningEffort?: V2ReviewStartResponse__ReasoningEffort | null; readonly receiverThreadIds: ReadonlyArray; readonly senderThreadId: string; - readonly status: "inProgress" | "completed" | "failed"; - readonly tool: "spawnAgent" | "sendInput" | "resumeAgent" | "wait" | "closeAgent"; + readonly status: "inProgress" | "completed" | "failed" | "interrupted"; + readonly tool: + | "spawnAgent" + | "sendInput" + | "resumeAgent" + | "wait" + | "closeAgent" + | "sendMessage" + | "followupTask" + | "interruptAgent" + | "listAgents"; readonly type: "collabAgentToolCall"; } | { @@ -22739,7 +22843,7 @@ export const V2ReviewStartResponse__ThreadItem = Schema.Union( senderThreadId: Schema.String.annotate({ description: "Thread ID of the agent issuing the collab request.", }), - status: Schema.Literals(["inProgress", "completed", "failed"]).annotate({ + status: Schema.Literals(["inProgress", "completed", "failed", "interrupted"]).annotate({ description: "Current status of the collab tool call.", }), tool: Schema.Literals([ @@ -22748,6 +22852,10 @@ export const V2ReviewStartResponse__ThreadItem = Schema.Union( "resumeAgent", "wait", "closeAgent", + "sendMessage", + "followupTask", + "interruptAgent", + "listAgents", ]).annotate({ description: "Name of the collab tool that was invoked." }), type: Schema.Literal("collabAgentToolCall").annotate({ title: "CollabAgentToolCallThreadItemType", @@ -22950,8 +23058,17 @@ export type V2ThreadForkResponse__ThreadItem = readonly reasoningEffort?: V2ThreadForkResponse__ReasoningEffort | null; readonly receiverThreadIds: ReadonlyArray; readonly senderThreadId: string; - readonly status: "inProgress" | "completed" | "failed"; - readonly tool: "spawnAgent" | "sendInput" | "resumeAgent" | "wait" | "closeAgent"; + readonly status: "inProgress" | "completed" | "failed" | "interrupted"; + readonly tool: + | "spawnAgent" + | "sendInput" + | "resumeAgent" + | "wait" + | "closeAgent" + | "sendMessage" + | "followupTask" + | "interruptAgent" + | "listAgents"; readonly type: "collabAgentToolCall"; } | { @@ -23175,7 +23292,7 @@ export const V2ThreadForkResponse__ThreadItem = Schema.Union( senderThreadId: Schema.String.annotate({ description: "Thread ID of the agent issuing the collab request.", }), - status: Schema.Literals(["inProgress", "completed", "failed"]).annotate({ + status: Schema.Literals(["inProgress", "completed", "failed", "interrupted"]).annotate({ description: "Current status of the collab tool call.", }), tool: Schema.Literals([ @@ -23184,6 +23301,10 @@ export const V2ThreadForkResponse__ThreadItem = Schema.Union( "resumeAgent", "wait", "closeAgent", + "sendMessage", + "followupTask", + "interruptAgent", + "listAgents", ]).annotate({ description: "Name of the collab tool that was invoked." }), type: Schema.Literal("collabAgentToolCall").annotate({ title: "CollabAgentToolCallThreadItemType", @@ -23355,8 +23476,17 @@ export type V2ThreadListResponse__ThreadItem = readonly reasoningEffort?: V2ThreadListResponse__ReasoningEffort | null; readonly receiverThreadIds: ReadonlyArray; readonly senderThreadId: string; - readonly status: "inProgress" | "completed" | "failed"; - readonly tool: "spawnAgent" | "sendInput" | "resumeAgent" | "wait" | "closeAgent"; + readonly status: "inProgress" | "completed" | "failed" | "interrupted"; + readonly tool: + | "spawnAgent" + | "sendInput" + | "resumeAgent" + | "wait" + | "closeAgent" + | "sendMessage" + | "followupTask" + | "interruptAgent" + | "listAgents"; readonly type: "collabAgentToolCall"; } | { @@ -23580,7 +23710,7 @@ export const V2ThreadListResponse__ThreadItem = Schema.Union( senderThreadId: Schema.String.annotate({ description: "Thread ID of the agent issuing the collab request.", }), - status: Schema.Literals(["inProgress", "completed", "failed"]).annotate({ + status: Schema.Literals(["inProgress", "completed", "failed", "interrupted"]).annotate({ description: "Current status of the collab tool call.", }), tool: Schema.Literals([ @@ -23589,6 +23719,10 @@ export const V2ThreadListResponse__ThreadItem = Schema.Union( "resumeAgent", "wait", "closeAgent", + "sendMessage", + "followupTask", + "interruptAgent", + "listAgents", ]).annotate({ description: "Name of the collab tool that was invoked." }), type: Schema.Literal("collabAgentToolCall").annotate({ title: "CollabAgentToolCallThreadItemType", @@ -23762,8 +23896,17 @@ export type V2ThreadMetadataUpdateResponse__ThreadItem = readonly reasoningEffort?: V2ThreadMetadataUpdateResponse__ReasoningEffort | null; readonly receiverThreadIds: ReadonlyArray; readonly senderThreadId: string; - readonly status: "inProgress" | "completed" | "failed"; - readonly tool: "spawnAgent" | "sendInput" | "resumeAgent" | "wait" | "closeAgent"; + readonly status: "inProgress" | "completed" | "failed" | "interrupted"; + readonly tool: + | "spawnAgent" + | "sendInput" + | "resumeAgent" + | "wait" + | "closeAgent" + | "sendMessage" + | "followupTask" + | "interruptAgent" + | "listAgents"; readonly type: "collabAgentToolCall"; } | { @@ -23990,7 +24133,7 @@ export const V2ThreadMetadataUpdateResponse__ThreadItem = Schema.Union( senderThreadId: Schema.String.annotate({ description: "Thread ID of the agent issuing the collab request.", }), - status: Schema.Literals(["inProgress", "completed", "failed"]).annotate({ + status: Schema.Literals(["inProgress", "completed", "failed", "interrupted"]).annotate({ description: "Current status of the collab tool call.", }), tool: Schema.Literals([ @@ -23999,6 +24142,10 @@ export const V2ThreadMetadataUpdateResponse__ThreadItem = Schema.Union( "resumeAgent", "wait", "closeAgent", + "sendMessage", + "followupTask", + "interruptAgent", + "listAgents", ]).annotate({ description: "Name of the collab tool that was invoked." }), type: Schema.Literal("collabAgentToolCall").annotate({ title: "CollabAgentToolCallThreadItemType", @@ -24170,8 +24317,17 @@ export type V2ThreadReadResponse__ThreadItem = readonly reasoningEffort?: V2ThreadReadResponse__ReasoningEffort | null; readonly receiverThreadIds: ReadonlyArray; readonly senderThreadId: string; - readonly status: "inProgress" | "completed" | "failed"; - readonly tool: "spawnAgent" | "sendInput" | "resumeAgent" | "wait" | "closeAgent"; + readonly status: "inProgress" | "completed" | "failed" | "interrupted"; + readonly tool: + | "spawnAgent" + | "sendInput" + | "resumeAgent" + | "wait" + | "closeAgent" + | "sendMessage" + | "followupTask" + | "interruptAgent" + | "listAgents"; readonly type: "collabAgentToolCall"; } | { @@ -24395,7 +24551,7 @@ export const V2ThreadReadResponse__ThreadItem = Schema.Union( senderThreadId: Schema.String.annotate({ description: "Thread ID of the agent issuing the collab request.", }), - status: Schema.Literals(["inProgress", "completed", "failed"]).annotate({ + status: Schema.Literals(["inProgress", "completed", "failed", "interrupted"]).annotate({ description: "Current status of the collab tool call.", }), tool: Schema.Literals([ @@ -24404,6 +24560,10 @@ export const V2ThreadReadResponse__ThreadItem = Schema.Union( "resumeAgent", "wait", "closeAgent", + "sendMessage", + "followupTask", + "interruptAgent", + "listAgents", ]).annotate({ description: "Name of the collab tool that was invoked." }), type: Schema.Literal("collabAgentToolCall").annotate({ title: "CollabAgentToolCallThreadItemType", @@ -24583,8 +24743,17 @@ export type V2ThreadResumeResponse__ThreadItem = readonly reasoningEffort?: V2ThreadResumeResponse__ReasoningEffort | null; readonly receiverThreadIds: ReadonlyArray; readonly senderThreadId: string; - readonly status: "inProgress" | "completed" | "failed"; - readonly tool: "spawnAgent" | "sendInput" | "resumeAgent" | "wait" | "closeAgent"; + readonly status: "inProgress" | "completed" | "failed" | "interrupted"; + readonly tool: + | "spawnAgent" + | "sendInput" + | "resumeAgent" + | "wait" + | "closeAgent" + | "sendMessage" + | "followupTask" + | "interruptAgent" + | "listAgents"; readonly type: "collabAgentToolCall"; } | { @@ -24808,7 +24977,7 @@ export const V2ThreadResumeResponse__ThreadItem = Schema.Union( senderThreadId: Schema.String.annotate({ description: "Thread ID of the agent issuing the collab request.", }), - status: Schema.Literals(["inProgress", "completed", "failed"]).annotate({ + status: Schema.Literals(["inProgress", "completed", "failed", "interrupted"]).annotate({ description: "Current status of the collab tool call.", }), tool: Schema.Literals([ @@ -24817,6 +24986,10 @@ export const V2ThreadResumeResponse__ThreadItem = Schema.Union( "resumeAgent", "wait", "closeAgent", + "sendMessage", + "followupTask", + "interruptAgent", + "listAgents", ]).annotate({ description: "Name of the collab tool that was invoked." }), type: Schema.Literal("collabAgentToolCall").annotate({ title: "CollabAgentToolCallThreadItemType", @@ -24988,8 +25161,17 @@ export type V2ThreadRollbackResponse__ThreadItem = readonly reasoningEffort?: V2ThreadRollbackResponse__ReasoningEffort | null; readonly receiverThreadIds: ReadonlyArray; readonly senderThreadId: string; - readonly status: "inProgress" | "completed" | "failed"; - readonly tool: "spawnAgent" | "sendInput" | "resumeAgent" | "wait" | "closeAgent"; + readonly status: "inProgress" | "completed" | "failed" | "interrupted"; + readonly tool: + | "spawnAgent" + | "sendInput" + | "resumeAgent" + | "wait" + | "closeAgent" + | "sendMessage" + | "followupTask" + | "interruptAgent" + | "listAgents"; readonly type: "collabAgentToolCall"; } | { @@ -25216,7 +25398,7 @@ export const V2ThreadRollbackResponse__ThreadItem = Schema.Union( senderThreadId: Schema.String.annotate({ description: "Thread ID of the agent issuing the collab request.", }), - status: Schema.Literals(["inProgress", "completed", "failed"]).annotate({ + status: Schema.Literals(["inProgress", "completed", "failed", "interrupted"]).annotate({ description: "Current status of the collab tool call.", }), tool: Schema.Literals([ @@ -25225,6 +25407,10 @@ export const V2ThreadRollbackResponse__ThreadItem = Schema.Union( "resumeAgent", "wait", "closeAgent", + "sendMessage", + "followupTask", + "interruptAgent", + "listAgents", ]).annotate({ description: "Name of the collab tool that was invoked." }), type: Schema.Literal("collabAgentToolCall").annotate({ title: "CollabAgentToolCallThreadItemType", @@ -25407,8 +25593,17 @@ export type V2ThreadStartedNotification__ThreadItem = readonly reasoningEffort?: V2ThreadStartedNotification__ReasoningEffort | null; readonly receiverThreadIds: ReadonlyArray; readonly senderThreadId: string; - readonly status: "inProgress" | "completed" | "failed"; - readonly tool: "spawnAgent" | "sendInput" | "resumeAgent" | "wait" | "closeAgent"; + readonly status: "inProgress" | "completed" | "failed" | "interrupted"; + readonly tool: + | "spawnAgent" + | "sendInput" + | "resumeAgent" + | "wait" + | "closeAgent" + | "sendMessage" + | "followupTask" + | "interruptAgent" + | "listAgents"; readonly type: "collabAgentToolCall"; } | { @@ -25635,7 +25830,7 @@ export const V2ThreadStartedNotification__ThreadItem = Schema.Union( senderThreadId: Schema.String.annotate({ description: "Thread ID of the agent issuing the collab request.", }), - status: Schema.Literals(["inProgress", "completed", "failed"]).annotate({ + status: Schema.Literals(["inProgress", "completed", "failed", "interrupted"]).annotate({ description: "Current status of the collab tool call.", }), tool: Schema.Literals([ @@ -25644,6 +25839,10 @@ export const V2ThreadStartedNotification__ThreadItem = Schema.Union( "resumeAgent", "wait", "closeAgent", + "sendMessage", + "followupTask", + "interruptAgent", + "listAgents", ]).annotate({ description: "Name of the collab tool that was invoked." }), type: Schema.Literal("collabAgentToolCall").annotate({ title: "CollabAgentToolCallThreadItemType", @@ -25815,8 +26014,17 @@ export type V2ThreadStartResponse__ThreadItem = readonly reasoningEffort?: V2ThreadStartResponse__ReasoningEffort | null; readonly receiverThreadIds: ReadonlyArray; readonly senderThreadId: string; - readonly status: "inProgress" | "completed" | "failed"; - readonly tool: "spawnAgent" | "sendInput" | "resumeAgent" | "wait" | "closeAgent"; + readonly status: "inProgress" | "completed" | "failed" | "interrupted"; + readonly tool: + | "spawnAgent" + | "sendInput" + | "resumeAgent" + | "wait" + | "closeAgent" + | "sendMessage" + | "followupTask" + | "interruptAgent" + | "listAgents"; readonly type: "collabAgentToolCall"; } | { @@ -26040,7 +26248,7 @@ export const V2ThreadStartResponse__ThreadItem = Schema.Union( senderThreadId: Schema.String.annotate({ description: "Thread ID of the agent issuing the collab request.", }), - status: Schema.Literals(["inProgress", "completed", "failed"]).annotate({ + status: Schema.Literals(["inProgress", "completed", "failed", "interrupted"]).annotate({ description: "Current status of the collab tool call.", }), tool: Schema.Literals([ @@ -26049,6 +26257,10 @@ export const V2ThreadStartResponse__ThreadItem = Schema.Union( "resumeAgent", "wait", "closeAgent", + "sendMessage", + "followupTask", + "interruptAgent", + "listAgents", ]).annotate({ description: "Name of the collab tool that was invoked." }), type: Schema.Literal("collabAgentToolCall").annotate({ title: "CollabAgentToolCallThreadItemType", @@ -26220,8 +26432,17 @@ export type V2ThreadUnarchiveResponse__ThreadItem = readonly reasoningEffort?: V2ThreadUnarchiveResponse__ReasoningEffort | null; readonly receiverThreadIds: ReadonlyArray; readonly senderThreadId: string; - readonly status: "inProgress" | "completed" | "failed"; - readonly tool: "spawnAgent" | "sendInput" | "resumeAgent" | "wait" | "closeAgent"; + readonly status: "inProgress" | "completed" | "failed" | "interrupted"; + readonly tool: + | "spawnAgent" + | "sendInput" + | "resumeAgent" + | "wait" + | "closeAgent" + | "sendMessage" + | "followupTask" + | "interruptAgent" + | "listAgents"; readonly type: "collabAgentToolCall"; } | { @@ -26448,7 +26669,7 @@ export const V2ThreadUnarchiveResponse__ThreadItem = Schema.Union( senderThreadId: Schema.String.annotate({ description: "Thread ID of the agent issuing the collab request.", }), - status: Schema.Literals(["inProgress", "completed", "failed"]).annotate({ + status: Schema.Literals(["inProgress", "completed", "failed", "interrupted"]).annotate({ description: "Current status of the collab tool call.", }), tool: Schema.Literals([ @@ -26457,6 +26678,10 @@ export const V2ThreadUnarchiveResponse__ThreadItem = Schema.Union( "resumeAgent", "wait", "closeAgent", + "sendMessage", + "followupTask", + "interruptAgent", + "listAgents", ]).annotate({ description: "Name of the collab tool that was invoked." }), type: Schema.Literal("collabAgentToolCall").annotate({ title: "CollabAgentToolCallThreadItemType", @@ -26630,8 +26855,17 @@ export type V2TurnCompletedNotification__ThreadItem = readonly reasoningEffort?: V2TurnCompletedNotification__ReasoningEffort | null; readonly receiverThreadIds: ReadonlyArray; readonly senderThreadId: string; - readonly status: "inProgress" | "completed" | "failed"; - readonly tool: "spawnAgent" | "sendInput" | "resumeAgent" | "wait" | "closeAgent"; + readonly status: "inProgress" | "completed" | "failed" | "interrupted"; + readonly tool: + | "spawnAgent" + | "sendInput" + | "resumeAgent" + | "wait" + | "closeAgent" + | "sendMessage" + | "followupTask" + | "interruptAgent" + | "listAgents"; readonly type: "collabAgentToolCall"; } | { @@ -26858,7 +27092,7 @@ export const V2TurnCompletedNotification__ThreadItem = Schema.Union( senderThreadId: Schema.String.annotate({ description: "Thread ID of the agent issuing the collab request.", }), - status: Schema.Literals(["inProgress", "completed", "failed"]).annotate({ + status: Schema.Literals(["inProgress", "completed", "failed", "interrupted"]).annotate({ description: "Current status of the collab tool call.", }), tool: Schema.Literals([ @@ -26867,6 +27101,10 @@ export const V2TurnCompletedNotification__ThreadItem = Schema.Union( "resumeAgent", "wait", "closeAgent", + "sendMessage", + "followupTask", + "interruptAgent", + "listAgents", ]).annotate({ description: "Name of the collab tool that was invoked." }), type: Schema.Literal("collabAgentToolCall").annotate({ title: "CollabAgentToolCallThreadItemType", @@ -27038,8 +27276,17 @@ export type V2TurnStartedNotification__ThreadItem = readonly reasoningEffort?: V2TurnStartedNotification__ReasoningEffort | null; readonly receiverThreadIds: ReadonlyArray; readonly senderThreadId: string; - readonly status: "inProgress" | "completed" | "failed"; - readonly tool: "spawnAgent" | "sendInput" | "resumeAgent" | "wait" | "closeAgent"; + readonly status: "inProgress" | "completed" | "failed" | "interrupted"; + readonly tool: + | "spawnAgent" + | "sendInput" + | "resumeAgent" + | "wait" + | "closeAgent" + | "sendMessage" + | "followupTask" + | "interruptAgent" + | "listAgents"; readonly type: "collabAgentToolCall"; } | { @@ -27266,7 +27513,7 @@ export const V2TurnStartedNotification__ThreadItem = Schema.Union( senderThreadId: Schema.String.annotate({ description: "Thread ID of the agent issuing the collab request.", }), - status: Schema.Literals(["inProgress", "completed", "failed"]).annotate({ + status: Schema.Literals(["inProgress", "completed", "failed", "interrupted"]).annotate({ description: "Current status of the collab tool call.", }), tool: Schema.Literals([ @@ -27275,6 +27522,10 @@ export const V2TurnStartedNotification__ThreadItem = Schema.Union( "resumeAgent", "wait", "closeAgent", + "sendMessage", + "followupTask", + "interruptAgent", + "listAgents", ]).annotate({ description: "Name of the collab tool that was invoked." }), type: Schema.Literal("collabAgentToolCall").annotate({ title: "CollabAgentToolCallThreadItemType", @@ -27446,8 +27697,17 @@ export type V2TurnStartResponse__ThreadItem = readonly reasoningEffort?: V2TurnStartResponse__ReasoningEffort | null; readonly receiverThreadIds: ReadonlyArray; readonly senderThreadId: string; - readonly status: "inProgress" | "completed" | "failed"; - readonly tool: "spawnAgent" | "sendInput" | "resumeAgent" | "wait" | "closeAgent"; + readonly status: "inProgress" | "completed" | "failed" | "interrupted"; + readonly tool: + | "spawnAgent" + | "sendInput" + | "resumeAgent" + | "wait" + | "closeAgent" + | "sendMessage" + | "followupTask" + | "interruptAgent" + | "listAgents"; readonly type: "collabAgentToolCall"; } | { @@ -27669,7 +27929,7 @@ export const V2TurnStartResponse__ThreadItem = Schema.Union( senderThreadId: Schema.String.annotate({ description: "Thread ID of the agent issuing the collab request.", }), - status: Schema.Literals(["inProgress", "completed", "failed"]).annotate({ + status: Schema.Literals(["inProgress", "completed", "failed", "interrupted"]).annotate({ description: "Current status of the collab tool call.", }), tool: Schema.Literals([ @@ -27678,6 +27938,10 @@ export const V2TurnStartResponse__ThreadItem = Schema.Union( "resumeAgent", "wait", "closeAgent", + "sendMessage", + "followupTask", + "interruptAgent", + "listAgents", ]).annotate({ description: "Name of the collab tool that was invoked." }), type: Schema.Literal("collabAgentToolCall").annotate({ title: "CollabAgentToolCallThreadItemType", @@ -35971,20 +36235,33 @@ export type ServerNotification__CollabAgentTool = | "sendInput" | "resumeAgent" | "wait" - | "closeAgent"; + | "closeAgent" + | "sendMessage" + | "followupTask" + | "interruptAgent" + | "listAgents"; export const ServerNotification__CollabAgentTool = Schema.Literals([ "spawnAgent", "sendInput", "resumeAgent", "wait", "closeAgent", + "sendMessage", + "followupTask", + "interruptAgent", + "listAgents", ]); -export type ServerNotification__CollabAgentToolCallStatus = "inProgress" | "completed" | "failed"; +export type ServerNotification__CollabAgentToolCallStatus = + | "inProgress" + | "completed" + | "failed" + | "interrupted"; export const ServerNotification__CollabAgentToolCallStatus = Schema.Literals([ "inProgress", "completed", "failed", + "interrupted", ]); export type ServerNotification__CommandExecOutputStream = "stdout" | "stderr"; @@ -38046,23 +38323,33 @@ export type V2ItemCompletedNotification__CollabAgentTool = | "sendInput" | "resumeAgent" | "wait" - | "closeAgent"; + | "closeAgent" + | "sendMessage" + | "followupTask" + | "interruptAgent" + | "listAgents"; export const V2ItemCompletedNotification__CollabAgentTool = Schema.Literals([ "spawnAgent", "sendInput", "resumeAgent", "wait", "closeAgent", + "sendMessage", + "followupTask", + "interruptAgent", + "listAgents", ]); export type V2ItemCompletedNotification__CollabAgentToolCallStatus = | "inProgress" | "completed" - | "failed"; + | "failed" + | "interrupted"; export const V2ItemCompletedNotification__CollabAgentToolCallStatus = Schema.Literals([ "inProgress", "completed", "failed", + "interrupted", ]); export type V2ItemCompletedNotification__CommandExecutionSource = @@ -38183,23 +38470,33 @@ export type V2ItemStartedNotification__CollabAgentTool = | "sendInput" | "resumeAgent" | "wait" - | "closeAgent"; + | "closeAgent" + | "sendMessage" + | "followupTask" + | "interruptAgent" + | "listAgents"; export const V2ItemStartedNotification__CollabAgentTool = Schema.Literals([ "spawnAgent", "sendInput", "resumeAgent", "wait", "closeAgent", + "sendMessage", + "followupTask", + "interruptAgent", + "listAgents", ]); export type V2ItemStartedNotification__CollabAgentToolCallStatus = | "inProgress" | "completed" - | "failed"; + | "failed" + | "interrupted"; export const V2ItemStartedNotification__CollabAgentToolCallStatus = Schema.Literals([ "inProgress", "completed", "failed", + "interrupted", ]); export type V2ItemStartedNotification__CommandExecutionSource = @@ -39200,23 +39497,33 @@ export type V2ReviewStartResponse__CollabAgentTool = | "sendInput" | "resumeAgent" | "wait" - | "closeAgent"; + | "closeAgent" + | "sendMessage" + | "followupTask" + | "interruptAgent" + | "listAgents"; export const V2ReviewStartResponse__CollabAgentTool = Schema.Literals([ "spawnAgent", "sendInput", "resumeAgent", "wait", "closeAgent", + "sendMessage", + "followupTask", + "interruptAgent", + "listAgents", ]); export type V2ReviewStartResponse__CollabAgentToolCallStatus = | "inProgress" | "completed" - | "failed"; + | "failed" + | "interrupted"; export const V2ReviewStartResponse__CollabAgentToolCallStatus = Schema.Literals([ "inProgress", "completed", "failed", + "interrupted", ]); export type V2ReviewStartResponse__CommandExecutionSource = @@ -39598,20 +39905,33 @@ export type V2ThreadForkResponse__CollabAgentTool = | "sendInput" | "resumeAgent" | "wait" - | "closeAgent"; + | "closeAgent" + | "sendMessage" + | "followupTask" + | "interruptAgent" + | "listAgents"; export const V2ThreadForkResponse__CollabAgentTool = Schema.Literals([ "spawnAgent", "sendInput", "resumeAgent", "wait", "closeAgent", + "sendMessage", + "followupTask", + "interruptAgent", + "listAgents", ]); -export type V2ThreadForkResponse__CollabAgentToolCallStatus = "inProgress" | "completed" | "failed"; +export type V2ThreadForkResponse__CollabAgentToolCallStatus = + | "inProgress" + | "completed" + | "failed" + | "interrupted"; export const V2ThreadForkResponse__CollabAgentToolCallStatus = Schema.Literals([ "inProgress", "completed", "failed", + "interrupted", ]); export type V2ThreadForkResponse__CommandExecutionSource = @@ -39955,20 +40275,33 @@ export type V2ThreadListResponse__CollabAgentTool = | "sendInput" | "resumeAgent" | "wait" - | "closeAgent"; + | "closeAgent" + | "sendMessage" + | "followupTask" + | "interruptAgent" + | "listAgents"; export const V2ThreadListResponse__CollabAgentTool = Schema.Literals([ "spawnAgent", "sendInput", "resumeAgent", "wait", "closeAgent", + "sendMessage", + "followupTask", + "interruptAgent", + "listAgents", ]); -export type V2ThreadListResponse__CollabAgentToolCallStatus = "inProgress" | "completed" | "failed"; +export type V2ThreadListResponse__CollabAgentToolCallStatus = + | "inProgress" + | "completed" + | "failed" + | "interrupted"; export const V2ThreadListResponse__CollabAgentToolCallStatus = Schema.Literals([ "inProgress", "completed", "failed", + "interrupted", ]); export type V2ThreadListResponse__CommandExecutionSource = @@ -40131,23 +40464,33 @@ export type V2ThreadMetadataUpdateResponse__CollabAgentTool = | "sendInput" | "resumeAgent" | "wait" - | "closeAgent"; + | "closeAgent" + | "sendMessage" + | "followupTask" + | "interruptAgent" + | "listAgents"; export const V2ThreadMetadataUpdateResponse__CollabAgentTool = Schema.Literals([ "spawnAgent", "sendInput", "resumeAgent", "wait", "closeAgent", + "sendMessage", + "followupTask", + "interruptAgent", + "listAgents", ]); export type V2ThreadMetadataUpdateResponse__CollabAgentToolCallStatus = | "inProgress" | "completed" - | "failed"; + | "failed" + | "interrupted"; export const V2ThreadMetadataUpdateResponse__CollabAgentToolCallStatus = Schema.Literals([ "inProgress", "completed", "failed", + "interrupted", ]); export type V2ThreadMetadataUpdateResponse__CommandExecutionSource = @@ -40265,20 +40608,33 @@ export type V2ThreadReadResponse__CollabAgentTool = | "sendInput" | "resumeAgent" | "wait" - | "closeAgent"; + | "closeAgent" + | "sendMessage" + | "followupTask" + | "interruptAgent" + | "listAgents"; export const V2ThreadReadResponse__CollabAgentTool = Schema.Literals([ "spawnAgent", "sendInput", "resumeAgent", "wait", "closeAgent", + "sendMessage", + "followupTask", + "interruptAgent", + "listAgents", ]); -export type V2ThreadReadResponse__CollabAgentToolCallStatus = "inProgress" | "completed" | "failed"; +export type V2ThreadReadResponse__CollabAgentToolCallStatus = + | "inProgress" + | "completed" + | "failed" + | "interrupted"; export const V2ThreadReadResponse__CollabAgentToolCallStatus = Schema.Literals([ "inProgress", "completed", "failed", + "interrupted", ]); export type V2ThreadReadResponse__CommandExecutionSource = @@ -40964,23 +41320,33 @@ export type V2ThreadResumeResponse__CollabAgentTool = | "sendInput" | "resumeAgent" | "wait" - | "closeAgent"; + | "closeAgent" + | "sendMessage" + | "followupTask" + | "interruptAgent" + | "listAgents"; export const V2ThreadResumeResponse__CollabAgentTool = Schema.Literals([ "spawnAgent", "sendInput", "resumeAgent", "wait", "closeAgent", + "sendMessage", + "followupTask", + "interruptAgent", + "listAgents", ]); export type V2ThreadResumeResponse__CollabAgentToolCallStatus = | "inProgress" | "completed" - | "failed"; + | "failed" + | "interrupted"; export const V2ThreadResumeResponse__CollabAgentToolCallStatus = Schema.Literals([ "inProgress", "completed", "failed", + "interrupted", ]); export type V2ThreadResumeResponse__CommandExecutionSource = @@ -41334,23 +41700,33 @@ export type V2ThreadRollbackResponse__CollabAgentTool = | "sendInput" | "resumeAgent" | "wait" - | "closeAgent"; + | "closeAgent" + | "sendMessage" + | "followupTask" + | "interruptAgent" + | "listAgents"; export const V2ThreadRollbackResponse__CollabAgentTool = Schema.Literals([ "spawnAgent", "sendInput", "resumeAgent", "wait", "closeAgent", + "sendMessage", + "followupTask", + "interruptAgent", + "listAgents", ]); export type V2ThreadRollbackResponse__CollabAgentToolCallStatus = | "inProgress" | "completed" - | "failed"; + | "failed" + | "interrupted"; export const V2ThreadRollbackResponse__CollabAgentToolCallStatus = Schema.Literals([ "inProgress", "completed", "failed", + "interrupted", ]); export type V2ThreadRollbackResponse__CommandExecutionSource = @@ -41673,23 +42049,33 @@ export type V2ThreadStartedNotification__CollabAgentTool = | "sendInput" | "resumeAgent" | "wait" - | "closeAgent"; + | "closeAgent" + | "sendMessage" + | "followupTask" + | "interruptAgent" + | "listAgents"; export const V2ThreadStartedNotification__CollabAgentTool = Schema.Literals([ "spawnAgent", "sendInput", "resumeAgent", "wait", "closeAgent", + "sendMessage", + "followupTask", + "interruptAgent", + "listAgents", ]); export type V2ThreadStartedNotification__CollabAgentToolCallStatus = | "inProgress" | "completed" - | "failed"; + | "failed" + | "interrupted"; export const V2ThreadStartedNotification__CollabAgentToolCallStatus = Schema.Literals([ "inProgress", "completed", "failed", + "interrupted", ]); export type V2ThreadStartedNotification__CommandExecutionSource = @@ -42075,23 +42461,33 @@ export type V2ThreadStartResponse__CollabAgentTool = | "sendInput" | "resumeAgent" | "wait" - | "closeAgent"; + | "closeAgent" + | "sendMessage" + | "followupTask" + | "interruptAgent" + | "listAgents"; export const V2ThreadStartResponse__CollabAgentTool = Schema.Literals([ "spawnAgent", "sendInput", "resumeAgent", "wait", "closeAgent", + "sendMessage", + "followupTask", + "interruptAgent", + "listAgents", ]); export type V2ThreadStartResponse__CollabAgentToolCallStatus = | "inProgress" | "completed" - | "failed"; + | "failed" + | "interrupted"; export const V2ThreadStartResponse__CollabAgentToolCallStatus = Schema.Literals([ "inProgress", "completed", "failed", + "interrupted", ]); export type V2ThreadStartResponse__CommandExecutionSource = @@ -42278,23 +42674,33 @@ export type V2ThreadUnarchiveResponse__CollabAgentTool = | "sendInput" | "resumeAgent" | "wait" - | "closeAgent"; + | "closeAgent" + | "sendMessage" + | "followupTask" + | "interruptAgent" + | "listAgents"; export const V2ThreadUnarchiveResponse__CollabAgentTool = Schema.Literals([ "spawnAgent", "sendInput", "resumeAgent", "wait", "closeAgent", + "sendMessage", + "followupTask", + "interruptAgent", + "listAgents", ]); export type V2ThreadUnarchiveResponse__CollabAgentToolCallStatus = | "inProgress" | "completed" - | "failed"; + | "failed" + | "interrupted"; export const V2ThreadUnarchiveResponse__CollabAgentToolCallStatus = Schema.Literals([ "inProgress", "completed", "failed", + "interrupted", ]); export type V2ThreadUnarchiveResponse__CommandExecutionSource = @@ -42412,23 +42818,33 @@ export type V2TurnCompletedNotification__CollabAgentTool = | "sendInput" | "resumeAgent" | "wait" - | "closeAgent"; + | "closeAgent" + | "sendMessage" + | "followupTask" + | "interruptAgent" + | "listAgents"; export const V2TurnCompletedNotification__CollabAgentTool = Schema.Literals([ "spawnAgent", "sendInput", "resumeAgent", "wait", "closeAgent", + "sendMessage", + "followupTask", + "interruptAgent", + "listAgents", ]); export type V2TurnCompletedNotification__CollabAgentToolCallStatus = | "inProgress" | "completed" - | "failed"; + | "failed" + | "interrupted"; export const V2TurnCompletedNotification__CollabAgentToolCallStatus = Schema.Literals([ "inProgress", "completed", "failed", + "interrupted", ]); export type V2TurnCompletedNotification__CommandExecutionSource = @@ -42524,23 +42940,33 @@ export type V2TurnStartedNotification__CollabAgentTool = | "sendInput" | "resumeAgent" | "wait" - | "closeAgent"; + | "closeAgent" + | "sendMessage" + | "followupTask" + | "interruptAgent" + | "listAgents"; export const V2TurnStartedNotification__CollabAgentTool = Schema.Literals([ "spawnAgent", "sendInput", "resumeAgent", "wait", "closeAgent", + "sendMessage", + "followupTask", + "interruptAgent", + "listAgents", ]); export type V2TurnStartedNotification__CollabAgentToolCallStatus = | "inProgress" | "completed" - | "failed"; + | "failed" + | "interrupted"; export const V2TurnStartedNotification__CollabAgentToolCallStatus = Schema.Literals([ "inProgress", "completed", "failed", + "interrupted", ]); export type V2TurnStartedNotification__CommandExecutionSource = @@ -42728,20 +43154,33 @@ export type V2TurnStartResponse__CollabAgentTool = | "sendInput" | "resumeAgent" | "wait" - | "closeAgent"; + | "closeAgent" + | "sendMessage" + | "followupTask" + | "interruptAgent" + | "listAgents"; export const V2TurnStartResponse__CollabAgentTool = Schema.Literals([ "spawnAgent", "sendInput", "resumeAgent", "wait", "closeAgent", + "sendMessage", + "followupTask", + "interruptAgent", + "listAgents", ]); -export type V2TurnStartResponse__CollabAgentToolCallStatus = "inProgress" | "completed" | "failed"; +export type V2TurnStartResponse__CollabAgentToolCallStatus = + | "inProgress" + | "completed" + | "failed" + | "interrupted"; export const V2TurnStartResponse__CollabAgentToolCallStatus = Schema.Literals([ "inProgress", "completed", "failed", + "interrupted", ]); export type V2TurnStartResponse__CommandExecutionSource = diff --git a/packages/effect-codex-app-server/src/schema.test.ts b/packages/effect-codex-app-server/src/schema.test.ts new file mode 100644 index 000000000..93935aa19 --- /dev/null +++ b/packages/effect-codex-app-server/src/schema.test.ts @@ -0,0 +1,73 @@ +import { assert, it } from "@effect/vitest"; +import * as Schema from "effect/Schema"; + +import * as CodexSchema from "./schema.ts"; + +it("accepts Codex 0.150 multi-agent values", () => { + const schemas = [ + CodexSchema.ServerNotification__SubAgentActivityKind, + CodexSchema.V2ItemStartedNotification__SubAgentActivityKind, + CodexSchema.V2ItemCompletedNotification__SubAgentActivityKind, + CodexSchema.V2ThreadReadResponse__SubAgentActivityKind, + CodexSchema.V2ThreadResumeResponse__SubAgentActivityKind, + ]; + + for (const schema of schemas) { + assert.equal(Schema.is(schema)("completed"), true); + } + + for (const tool of ["sendMessage", "followupTask", "interruptAgent", "listAgents"]) { + assert.equal(Schema.is(CodexSchema.ServerNotification__CollabAgentTool)(tool), true); + assert.equal(Schema.is(CodexSchema.V2ThreadResumeResponse__CollabAgentTool)(tool), true); + } + + assert.equal( + Schema.is(CodexSchema.ServerNotification__CollabAgentToolCallStatus)("interrupted"), + true, + ); + assert.equal( + Schema.is(CodexSchema.V2ThreadResumeResponse__CollabAgentToolCallStatus)("interrupted"), + true, + ); + + const resumeResponse = { + approvalPolicy: "never", + approvalsReviewer: "user", + cwd: "/tmp/project", + model: "gpt-5.6-sol", + modelProvider: "openai", + sandbox: { type: "dangerFullAccess" }, + thread: { + cliVersion: "0.150.0", + createdAt: 0, + cwd: "/tmp/project", + ephemeral: false, + id: "root-thread", + modelProvider: "openai", + preview: "", + sessionId: "session-1", + source: "cli", + status: { type: "idle" }, + turns: [ + { + id: "turn-1", + status: "completed", + items: [ + { + agentsStates: {}, + id: "item-1", + receiverThreadIds: ["child-thread"], + senderThreadId: "root-thread", + status: "interrupted", + tool: "followupTask", + type: "collabAgentToolCall", + }, + ], + }, + ], + updatedAt: 0, + }, + }; + + assert.equal(Schema.is(CodexSchema.V2ThreadResumeResponse)(resumeResponse), true); +});