Compare commits

..
Author SHA1 Message Date
buzzert 730d609f81 Relax Xcode signing identity matching
TestFlight / testflight (push) Failing after 26s
2026-06-25 23:57:17 -07:00
buzzert 137fce8558 Use passworded CI keychain
TestFlight / testflight (push) Failing after 25s
2026-06-25 23:53:24 -07:00
buzzert 3e6d3c6817 Use user keychain domain in CI
TestFlight / testflight (push) Failing after 23s
2026-06-25 23:51:33 -07:00
buzzert 100b51de12 Use explicit CI signing keychain
TestFlight / testflight (push) Failing after 18s
2026-06-25 23:48:26 -07:00
buzzert 23ee30a53a Simplify TestFlight CI signing
TestFlight / testflight (push) Failing after 22s
2026-06-25 23:44:13 -07:00
buzzert 0be2442ad0 Pass signing keychain to Xcode resolver
TestFlight / testflight (push) Failing after 28s
2026-06-25 23:37:24 -07:00
buzzert c84ef8c242 Refresh CI key partition access before build
TestFlight / testflight (push) Failing after 27s
2026-06-25 23:35:00 -07:00
buzzert 98f96eda45 Let Xcode select Apple Distribution identity
TestFlight / testflight (push) Failing after 25s
2026-06-25 23:32:38 -07:00
buzzert 3904457c21 Use runner home for CI keychain preferences
TestFlight / testflight (push) Failing after 24s
2026-06-25 23:30:46 -07:00
buzzert 0fc2117a11 Set CI keychain as default for Xcode
TestFlight / testflight (push) Failing after 18s
2026-06-25 23:28:58 -07:00
buzzert 60469f05b5 Tolerate login keychain preference failure
TestFlight / testflight (push) Failing after 23s
2026-06-25 23:27:13 -07:00
buzzert d834ed7931 Create CI login keychain when missing
TestFlight / testflight (push) Failing after 19s
2026-06-25 23:25:42 -07:00
buzzert f98a002f52 Use explicit runner login keychain
TestFlight / testflight (push) Failing after 17s
2026-06-25 23:23:13 -07:00
buzzert b0c0a2d55e Reset CI keychain search list
TestFlight / testflight (push) Failing after 19s
2026-06-25 23:21:42 -07:00
buzzert 3262f4ff80 Detect runner login keychain path
TestFlight / testflight (push) Failing after 19s
2026-06-25 23:20:06 -07:00
buzzert 585be09eb7 Target login keychain path for CI signing
TestFlight / testflight (push) Failing after 19s
2026-06-25 23:18:01 -07:00
buzzert 387896741c Use runner login keychain for CI signing
TestFlight / testflight (push) Failing after 21s
2026-06-25 23:16:22 -07:00
buzzert f6a10af7a9 Use signing certificate identity hash
TestFlight / testflight (push) Failing after 24s
2026-06-25 23:13:08 -07:00
buzzert 8aab86e2a6 Avoid changing default keychain in CI
TestFlight / testflight (push) Failing after 25s
2026-06-25 23:10:35 -07:00
buzzert eb4b233e33 Resolve CI signing keychain path
TestFlight / testflight (push) Failing after 18s
2026-06-25 23:08:35 -07:00
buzzert cbd7a68e57 Make CI signing keychain visible to Xcode
TestFlight / testflight (push) Failing after 21s
2026-06-25 23:06:00 -07:00
buzzert 04c15e8f12 Use absolute iOS paths in Fastlane
TestFlight / testflight (push) Failing after 25s
2026-06-25 22:50:30 -07:00
buzzert ca28ebc0a0 Use disposable match keychain in CI
TestFlight / testflight (push) Failing after 16s
2026-06-25 22:48:59 -07:00
buzzert 87787642b5 Preserve Ruby path for TestFlight workflow
TestFlight / testflight (push) Failing after 22s
2026-06-25 22:46:14 -07:00
buzzert 4124a31a34 Use Ruby 3.1 for TestFlight workflow
TestFlight / testflight (push) Failing after 21s
2026-06-25 22:43:27 -07:00
buzzert a68f1e50ca Reset iOS TestFlight deployment
TestFlight / testflight (push) Failing after 14s
2026-06-25 22:41:00 -07:00
buzzert 272ad0bbf0 ios: pass signing settings to archive
TestFlight Release / testflight (push) Failing after 17s
2026-06-25 22:19:25 -07:00
buzzert de7b448bc5 ios: avoid system default keychain writes
TestFlight Release / testflight (push) Failing after 16s
2026-06-25 22:16:24 -07:00
buzzert 3c7fc51fdb ios: set ci keychain in default domain
TestFlight Release / testflight (push) Failing after 10s
2026-06-25 22:14:25 -07:00
buzzert 0062f37b9f ios: sign with disposable login keychain
TestFlight Release / testflight (push) Failing after 17s
2026-06-25 22:12:17 -07:00
buzzert 0ae551615f ios: use signing identity fingerprint in ci
TestFlight Release / testflight (push) Failing after 16s
2026-06-25 22:10:06 -07:00
buzzert 88bef50ae7 ios: create named ci keychain in home
TestFlight Release / testflight (push) Failing after 15s
2026-06-25 22:07:12 -07:00
buzzert 0d069b4233 ios: create ci keychain by name
TestFlight Release / testflight (push) Failing after 11s
2026-06-25 22:05:47 -07:00
buzzert 60bbe077e8 ios: pass signing keychain to xcode
TestFlight Release / testflight (push) Failing after 18s
2026-06-25 22:02:19 -07:00
buzzert 0b09d5425b ios: handle empty ci keychain list
TestFlight Release / testflight (push) Failing after 15s
2026-06-25 21:58:01 -07:00
buzzert c9a3015e35 ios: parse ci profile without keychain
TestFlight Release / testflight (push) Failing after 9s
2026-06-25 21:56:19 -07:00
buzzert abd8a80daa ios: isolate ci signing keychains
TestFlight Release / testflight (push) Failing after 8s
2026-06-25 21:52:17 -07:00
buzzert 0f76ef91a9 ios: restore working ci p12 import
TestFlight Release / testflight (push) Failing after 9s
2026-06-25 21:48:19 -07:00
buzzert 72e2ffd898 ios: use temporary keychain path in ci
TestFlight Release / testflight (push) Failing after 9s
2026-06-25 21:46:48 -07:00
buzzert 4c610c89e1 ios: install ci profiles for xcode signing
TestFlight Release / testflight (push) Failing after 9s
2026-06-25 21:44:42 -07:00
buzzert 477921563f ios: remove invalid ci codesign path
TestFlight Release / testflight (push) Failing after 18s
2026-06-25 21:36:37 -07:00
buzzert 0fca0e93ec ios: grant ci key access to xcode tools
TestFlight Release / testflight (push) Failing after 10s
2026-06-25 21:35:11 -07:00
buzzert f977f9943c ios: patch generated release signing settings
TestFlight Release / testflight (push) Failing after 16s
2026-06-25 21:31:51 -07:00
buzzert f445730a41 ios: override iphoneos signing identity
TestFlight Release / testflight (push) Failing after 16s
2026-06-25 21:29:35 -07:00
buzzert 76cb808c33 ios: use disposable keychain as ci default
TestFlight Release / testflight (push) Failing after 15s
2026-06-25 21:27:19 -07:00
buzzert e167bd983f ios: use generic xcode signing selector
TestFlight Release / testflight (push) Failing after 19s
2026-06-25 21:25:13 -07:00
buzzert e4dd91564f ios: unlock signing keychain before build
TestFlight Release / testflight (push) Failing after 17s
2026-06-25 21:20:31 -07:00
buzzert 3bfde476a6 ios: use single identity signing p12
TestFlight Release / testflight (push) Failing after 16s
2026-06-25 21:18:54 -07:00
buzzert b8676027db ios: trust Apple root in CI signing keychain
TestFlight Release / testflight (push) Failing after 8s
2026-06-25 21:12:53 -07:00
buzzert d36d2c60a3 ios: install Apple WWDR intermediate in CI
TestFlight Release / testflight (push) Failing after 18s
2026-06-25 21:11:01 -07:00
buzzert 3d7031bb40 ios: avoid default keychain mutation in ci
TestFlight Release / testflight (push) Failing after 17s
2026-06-25 21:08:32 -07:00
buzzert fa9b725c77 ios: expose signing keychain to xcodebuild
TestFlight Release / testflight (push) Failing after 9s
2026-06-25 21:07:38 -07:00
buzzert a88987d08d ios: pin distribution signing identity
TestFlight Release / testflight (push) Failing after 15s
2026-06-25 21:05:26 -07:00
buzzert e137ea1077 ios: bootstrap signing with existing certificate
TestFlight Release / testflight (push) Failing after 17s
2026-06-25 21:03:43 -07:00
buzzert fad25d7f2b ios: configure api-key TestFlight signing 2026-06-25 20:51:01 -07:00
buzzert fb28508764 ios: ci: keychain cleanup 2026-06-25 20:35:39 -07:00
buzzert 4365798f5e workflow: fix
TestFlight Release / testflight (push) Failing after 16s
2026-06-25 20:21:39 -07:00
12 changed files with 163 additions and 274 deletions
+19 -8
View File
@@ -4,11 +4,11 @@ on:
workflow_dispatch:
push:
tags:
- "release/ios/v*"
- "v*"
jobs:
testflight:
runs-on: macos-arm64
runs-on: xcode
defaults:
run:
shell: bash
@@ -33,24 +33,35 @@ jobs:
brew install xcodegen
fi
- name: Runner diagnostics
- name: Prepare Runner Keychain
env:
HOME: /var/lib/act_runner
run: |
set -euo pipefail
whoami
printf 'HOME=%s\n' "$HOME"
security default-keychain -d user || true
security list-keychains -d user || true
mkdir -p "${HOME}/Library/Keychains"
login_keychain="${HOME}/Library/Keychains/login.keychain"
if [ ! -f "${login_keychain}-db" ]; then
security create-keychain -p "" "${login_keychain}"
fi
security unlock-keychain -p "" "${login_keychain}" 2>/dev/null || \
security unlock-keychain -p "sybil-ci-keychain-password" "${login_keychain}" 2>/dev/null || true
security default-keychain -d user -s "${login_keychain}"
security list-keychains -d user -s "${login_keychain}-db"
security delete-keychain "${HOME}/Library/Keychains/sybil_ci_keychain" >/dev/null 2>&1 || true
rm -f "${HOME}/Library/Keychains/sybil_ci_keychain" "${HOME}/Library/Keychains/sybil_ci_keychain-db"
- name: Upload to TestFlight
working-directory: ios
env:
HOME: /var/lib/act_runner
APP_STORE_CONNECT_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }}
APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
APP_STORE_CONNECT_KEY_CONTENT: ${{ secrets.APP_STORE_CONNECT_KEY_CONTENT }}
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
MATCH_GIT_URL: ${{ secrets.MATCH_GIT_URL }}
MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GIT_BASIC_AUTHORIZATION }}
SYBIL_BUILD_NUMBER: ${{ github.run_number }}
FASTLANE_SKIP_UPDATE_CHECK: "1"
FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT: "120"
run: |
-1
View File
@@ -285,7 +285,6 @@ Behavior notes:
- For `chatId` calls, server stores only *new* non-assistant messages from provided history to avoid duplicates.
- `additionalSystemPrompt`, when present directly or loaded from stored chat settings, is prepended to the provider request as a `system` message and is not inserted into the persisted chat transcript by this endpoint.
- `enabledTools` limits Sybil-managed tools for this request. When omitted for a saved chat, the stored chat setting is used; otherwise all available tools are enabled by default. An empty array disables Sybil-managed tools.
- `maxTokens` is optional. For `anthropic`, when omitted the backend requests the selected model's maximum output token limit from Anthropic's Models API and uses that as `max_tokens`; if the model limit cannot be loaded, the fallback is 128000. For other providers, omitted `maxTokens` is not sent as an explicit cap.
- Server persists final assistant output and call metadata (`LlmCall`) in DB.
- Server updates chat-level model metadata on each call: `lastUsedProvider`/`lastUsedModel`; first successful/failed call also initializes `initiatedProvider`/`initiatedModel` if unset.
- Attachments are optional and currently apply to `user` messages. Persisted chat history stores them under `message.metadata.attachments`.
-1
View File
@@ -64,7 +64,6 @@ Notes:
- For persisted streams, backend stores only new non-assistant input history rows to avoid duplicates.
- `additionalSystemPrompt`, when present directly or loaded from stored chat settings, is prepended to the provider request as a `system` message and is not inserted into the persisted chat transcript by this endpoint.
- `enabledTools` limits Sybil-managed tools for this request. When omitted for a saved chat, the stored chat setting is used; otherwise all available tools are enabled by default. An empty array disables Sybil-managed tools.
- `maxTokens` is optional. For `anthropic`, when omitted the backend requests the selected model's maximum output token limit from Anthropic's Models API and uses that as `max_tokens`; if the model limit cannot be loaded, the fallback is 128000. For other providers, omitted `maxTokens` is not sent as an explicit cap.
- Attachments are optional and are persisted under `message.metadata.attachments` on stored user messages when `persist` is `true`.
Persisted chat streams with a `chatId` are backend-owned active runs:
-6
View File
@@ -21,12 +21,6 @@ Instructions for work under `/Users/buzzert/src/sybil-2/ios`.
- To choose a screenshot path, run `just screenshot path=build/name.png`.
- The underlying screenshot command is `xcrun simctl io booted screenshot <path>` and requires a booted simulator.
## Release Workflow
- iOS release tags use the annotated tag namespace `release/ios/vX.Y.Z`; increment from the latest existing `release/ios/v*` tag.
- Tag message convention is `ios: X.Y.Z`, for example `git tag -a release/ios/v1.13.5 -m "ios: 1.13.5"`.
- Push the release commit and tag together with `git push origin <branch> release/ios/vX.Y.Z`.
- Fastlane derives the marketing version from the release tag and stamps `ios/Apps/Sybil/project.yml` during CI, so do not manually bump `MARKETING_VERSION` for normal tagged releases unless explicitly requested.
## App Structure
- App target entry: `/Users/buzzert/src/sybil-2/ios/Apps/Sybil/Sources/SybilApp.swift`
- Shared iOS app code lives in Swift package:
+1 -1
View File
@@ -24,7 +24,7 @@ targets:
GENERATE_INFOPLIST_FILE: YES
INFOPLIST_FILE: Apps/Sybil/Info.plist
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
MARKETING_VERSION: "1.13.2"
MARKETING_VERSION: "1.10"
CURRENT_PROJECT_VERSION: 11
INFOPLIST_KEY_CFBundleDisplayName: Sybil
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption: NO
@@ -58,8 +58,7 @@ struct SybilChatTranscriptView: View {
.frame(height: 18 + bottomContentInset)
.id(bottomAnchorID)
}
.frame(maxWidth: SybilLayout.webContentMaxWidth, alignment: .leading)
.frame(maxWidth: .infinity, alignment: .center)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 14)
.padding(.top, 18 + topContentInset)
}
@@ -98,8 +98,7 @@ struct SybilSearchResultsView: View {
.foregroundStyle(SybilTheme.danger)
}
}
.frame(maxWidth: SybilLayout.webContentMaxWidth, alignment: .leading)
.frame(maxWidth: .infinity, alignment: .center)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 14)
.padding(.top, 20 + topContentInset)
.padding(.bottom, 20 + bottomContentInset)
@@ -64,10 +64,6 @@ extension Font {
}
}
enum SybilLayout {
static let webContentMaxWidth: CGFloat = 896
}
enum SybilTheme {
static let background = Color(red: 0.02, green: 0.02, blue: 0.05)
static let surface = Color(red: 0.05, green: 0.04, blue: 0.10)
+118 -68
View File
@@ -1,3 +1,4 @@
require "fileutils"
require "shellwords"
default_platform(:ios)
@@ -6,56 +7,41 @@ APP_IDENTIFIER = "net.buzzert.sybil2"
SCHEME = "Sybil"
TEAM_ID = "DQQH5H6GBD"
PROFILE_NAME = "Sybil AppStore CI"
SIGNING_IDENTITY = "Apple Distribution: James Magahern (DQQH5H6GBD)"
CI_KEYCHAIN_NAME = "sybil_ci_keychain"
CI_KEYCHAIN_PASSWORD = "sybil-ci-keychain-password"
IOS_ROOT = File.expand_path("..", __dir__)
PROJECT_FILE = File.join(IOS_ROOT, "Sybil.xcodeproj")
PROJECT_SPEC = File.join(IOS_ROOT, "project.yml")
APP_PROJECT_SPEC = File.join(IOS_ROOT, "Apps/Sybil/project.yml")
CI_KEYCHAIN_PATH = File.join(File.expand_path("~/Library/Keychains"), CI_KEYCHAIN_NAME)
CI_KEYCHAIN_DB_PATH = "#{CI_KEYCHAIN_PATH}-db"
LOGIN_KEYCHAIN_PATH = File.expand_path("~/Library/Keychains/login.keychain")
LOGIN_KEYCHAIN_DB_PATH = "#{LOGIN_KEYCHAIN_PATH}-db"
def present?(value)
!value.to_s.strip.empty?
end
def release_version
tag = ENV["SYBIL_VERSION_TAG"].to_s
tag = ENV["GITHUB_REF_NAME"].to_s if !present?(tag)
tag = ENV["GITHUB_REF"].to_s.sub(%r{\Arefs/tags/}, "") if !present?(tag)
tag = sh("git describe --tags --abbrev=0").strip if !present?(tag)
version = tag.sub(%r{\Arelease/}, "").sub(/\Av/, "")
unless version.match?(/\A\d+\.\d+\.\d+\z/)
UI.user_error!("Release tag must look like v1.2.3; got #{tag.inspect}")
end
version
end
def ci?
present?(ENV["CI"])
end
def release_version
tag = ENV["SYBIL_VERSION_TAG"]
tag = ENV["GITHUB_REF_NAME"] if !present?(tag)
tag = ENV["GITHUB_REF"].to_s.sub(%r{\Arefs/tags/}, "") if !present?(tag)
tag = sh("git describe --tags --abbrev=0").strip if !present?(tag)
match = tag.to_s.match(%r{\Arelease/ios/v(\d+\.\d+\.\d+)\z})
unless match
UI.user_error!("Release tag must look like release/ios/v1.2.3; got #{tag.inspect}")
end
match[1]
end
# App Store Connect requires CFBundleVersion to be unique and strictly
# increasing app-wide (not just per marketing version), so we derive it from
# the monotonic CI run number rather than querying TestFlight (that query can
# lag behind builds still processing and hand back a colliding value).
def build_number
value = present?(ENV["SYBIL_BUILD_NUMBER"]) ? ENV["SYBIL_BUILD_NUMBER"] : ENV["GITHUB_RUN_NUMBER"]
unless value.to_s.match?(/\A\d+\z/)
UI.user_error!("Build number must come from SYBIL_BUILD_NUMBER/GITHUB_RUN_NUMBER; got #{value.inspect}")
end
value.to_i
end
def stamp_marketing_version(version)
contents = File.read(APP_PROJECT_SPEC)
updated = contents.sub(/^(\s*MARKETING_VERSION:\s*).*/, "\\1\"#{version}\"")
if updated == contents
UI.user_error!("Could not find MARKETING_VERSION in #{APP_PROJECT_SPEC}")
end
File.write(APP_PROJECT_SPEC, updated)
def ci_keychain_path
File.file?(CI_KEYCHAIN_DB_PATH) ? CI_KEYCHAIN_DB_PATH : CI_KEYCHAIN_PATH
end
platform :ios do
@@ -68,24 +54,54 @@ platform :ios do
)
end
# CI uses a throwaway keychain that is deleted after the lane exits.
private_lane :prepare_ci_keychain do
next nil unless ci?
private_lane :setup_ci_signing do
next unless ci?
run_token = "#{Time.now.to_i}_#{Process.pid}"
keychain_name = "fastlane_ci_#{run_token}"
FileUtils.mkdir_p(File.dirname(CI_KEYCHAIN_PATH))
sh("security delete-keychain #{CI_KEYCHAIN_PATH.shellescape} || true", log: false)
FileUtils.rm_f(CI_KEYCHAIN_PATH)
FileUtils.rm_f(CI_KEYCHAIN_DB_PATH)
setup_ci(
force: true,
keychain_name: keychain_name,
timeout: 7_200
create_keychain(
path: CI_KEYCHAIN_PATH,
password: CI_KEYCHAIN_PASSWORD,
default_keychain: false,
unlock: true,
timeout: 3600,
lock_when_sleeps: true,
add_to_search_list: false
)
keychain_name
sh("security default-keychain -d user -s #{CI_KEYCHAIN_PATH.shellescape}", log: false)
sh("security list-keychains -d user -s #{ci_keychain_path.shellescape}", log: false)
sh("security list-keychains -d dynamic -s #{ci_keychain_path.shellescape} || true", log: false)
sh("security list-keychains -d common -s #{ci_keychain_path.shellescape} || true", log: false)
ENV["MATCH_KEYCHAIN_NAME"] = CI_KEYCHAIN_PATH
ENV["MATCH_KEYCHAIN_PASSWORD"] = CI_KEYCHAIN_PASSWORD
ENV["MATCH_READONLY"] = "true"
end
private_lane :cleanup_ci_signing do
next unless ci?
if File.file?(LOGIN_KEYCHAIN_DB_PATH) || File.file?(LOGIN_KEYCHAIN_PATH)
sh("security default-keychain -d user -s #{LOGIN_KEYCHAIN_PATH.shellescape} || true", log: false)
sh("security list-keychains -d user -s #{LOGIN_KEYCHAIN_DB_PATH.shellescape} || true", log: false)
end
sh("security delete-keychain #{ci_keychain_path.shellescape} || true", log: false)
FileUtils.rm_f(CI_KEYCHAIN_PATH)
FileUtils.rm_f(CI_KEYCHAIN_DB_PATH)
rescue => error
UI.message("Unable to delete temporary CI keychain: #{error.message}")
ensure
ENV.delete("MATCH_KEYCHAIN_NAME")
ENV.delete("MATCH_KEYCHAIN_PASSWORD")
ENV.delete("MATCH_READONLY")
end
private_lane :sync_signing do |options|
match(
match_options = {
type: "appstore",
readonly: options.fetch(:readonly),
app_identifier: APP_IDENTIFIER,
@@ -96,7 +112,28 @@ platform :ios do
git_full_name: "Sybil Release Bot",
git_user_email: "james.magahern@me.com",
api_key: options.fetch(:api_key)
)
}
match_options[:keychain_name] = ENV["MATCH_KEYCHAIN_NAME"] if present?(ENV["MATCH_KEYCHAIN_NAME"])
match_options[:keychain_password] = ENV["MATCH_KEYCHAIN_PASSWORD"] if ENV.key?("MATCH_KEYCHAIN_PASSWORD")
match(match_options)
end
private_lane :verify_ci_signing do
next unless ci?
if File.file?(ci_keychain_path)
password = ENV.fetch("MATCH_KEYCHAIN_PASSWORD", "")
sh("security unlock-keychain -p #{password.shellescape} #{ci_keychain_path.shellescape}", log: false)
sh("security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k #{password.shellescape} #{ci_keychain_path.shellescape}", log: false)
end
identities = sh("security find-identity -v -p codesigning #{ci_keychain_path.shellescape}", log: false)
UI.message(identities)
unless identities.include?(SIGNING_IDENTITY)
UI.user_error!("The CI keychain search list does not contain #{SIGNING_IDENTITY}")
end
end
desc "Create or update match signing assets"
@@ -106,35 +143,49 @@ platform :ios do
desc "Build and upload to TestFlight"
lane :beta do
ci_keychain_name = nil
begin
ci_keychain_name = prepare_ci_keychain
setup_ci_signing
api_key = app_store_api_key
version = release_version
stamp_marketing_version(version)
sh("xcodegen", "--spec", PROJECT_SPEC)
sh("xcodegen --spec #{PROJECT_SPEC.shellescape}")
increment_version_number(version_number: version, xcodeproj: PROJECT_FILE)
increment_build_number(build_number: build_number, xcodeproj: PROJECT_FILE)
increment_version_number(
version_number: release_version,
xcodeproj: PROJECT_FILE
)
latest_build_number = latest_testflight_build_number(
app_identifier: APP_IDENTIFIER,
api_key: api_key,
initial_build_number: 0
)
increment_build_number(
build_number: latest_build_number + 1,
xcodeproj: PROJECT_FILE
)
sync_signing(api_key: api_key, readonly: true)
verify_ci_signing
sh("security find-identity -v -p codesigning") if ci_keychain_name
xcargs = [
"DEVELOPMENT_TEAM=#{TEAM_ID.shellescape}",
"CODE_SIGN_STYLE=Manual",
"CODE_SIGN_IDENTITY=Apple\\ Distribution",
"PROVISIONING_PROFILE_SPECIFIER=#{PROFILE_NAME.shellescape}"
]
if ci?
xcargs << "CODE_SIGN_KEYCHAIN=#{ci_keychain_path.shellescape}"
xcargs << "OTHER_CODE_SIGN_FLAGS=#{("--keychain #{ci_keychain_path}").shellescape}"
end
build_app(
project: PROJECT_FILE,
scheme: SCHEME,
export_method: "app-store",
codesigning_identity: "Apple Distribution",
xcargs: [
"DEVELOPMENT_TEAM=#{TEAM_ID.shellescape}",
"CODE_SIGN_STYLE=Manual",
"CODE_SIGN_IDENTITY=Apple\\ Distribution",
"PROVISIONING_PROFILE_SPECIFIER=#{PROFILE_NAME.shellescape}"
].join(" "),
xcargs: xcargs.join(" "),
export_options: {
signingStyle: "manual",
teamID: TEAM_ID,
@@ -149,7 +200,6 @@ platform :ios do
skip_waiting_for_build_processing: true
)
ensure
delete_keychain(name: ci_keychain_name) if ci_keychain_name
end
cleanup_ci_signing
end
end
+4 -45
View File
@@ -28,45 +28,6 @@ import type { ChatMessage } from "../types.js";
const INTERNAL_CORRECTION =
"Internal correction: the previous assistant message claimed it would run a tool, but no tool call was made. If the task needs an available tool, call it now. Otherwise provide the final answer directly without saying you will run a tool.";
const DEFAULT_ANTHROPIC_MAX_TOKENS = 128_000;
const MODEL_MAX_TOKENS_CACHE_MS = 24 * 60 * 60 * 1000;
const modelMaxTokensCache = new Map<string, { maxTokens: number; expiresAt: number }>();
function readMaxTokens(value: unknown) {
return Number.isSafeInteger(value) && (value as number) > 0 ? (value as number) : undefined;
}
function getModelInfoMaxTokens(modelInfo: any) {
return readMaxTokens(modelInfo?.max_tokens) ?? readMaxTokens(modelInfo?.maxTokens);
}
async function getMessagesMaxTokens(params: ToolAwareCompletionParams) {
if (params.maxTokens) return params.maxTokens;
const cached = modelMaxTokensCache.get(params.model);
if (cached && cached.expiresAt > Date.now()) return cached.maxTokens;
try {
const retrieve = params.client?.models?.retrieve;
if (typeof retrieve === "function") {
const modelInfo = await retrieve.call(params.client.models, params.model);
const maxTokens = getModelInfoMaxTokens(modelInfo);
if (maxTokens) {
modelMaxTokensCache.set(params.model, {
maxTokens,
expiresAt: Date.now() + MODEL_MAX_TOKENS_CACHE_MS,
});
return maxTokens;
}
}
} catch {
// Fall back to the documented max for Claude Opus 4.8 and related high-output models.
}
return DEFAULT_ANTHROPIC_MAX_TOKENS;
}
function toTools(tools: any[]) {
return tools
.map((tool) => {
@@ -199,12 +160,11 @@ function mergeUsage(acc: Required<ToolAwareUsage>, usage: any) {
export async function completeWithMessagesApi(params: ToolAwareCompletionParams): Promise<ToolAwareCompletionResult> {
const enabledTools = getEnabledChatTools(params);
const maxTokens = await getMessagesMaxTokens(params);
if (!enabledTools.length) {
const response = await params.client.messages.create({
model: params.model,
system: buildTopLevelSystemPrompt(params.messages, params.userLocation),
max_tokens: maxTokens,
max_tokens: params.maxTokens ?? 1024,
temperature: params.temperature,
messages: buildBaseMessages(params),
} as any);
@@ -232,7 +192,7 @@ export async function completeWithMessagesApi(params: ToolAwareCompletionParams)
const response = await params.client.messages.create({
model: params.model,
system: buildTopLevelSystemPrompt(params.messages, params.userLocation, buildChatToolSystemPrompt(params)),
max_tokens: maxTokens,
max_tokens: params.maxTokens ?? 1024,
temperature: params.temperature,
messages: conversation,
tools: toTools(enabledTools),
@@ -288,7 +248,6 @@ export async function completeWithMessagesApi(params: ToolAwareCompletionParams)
export async function* streamWithMessagesApi(params: ToolAwareCompletionParams): AsyncGenerator<ToolAwareStreamingEvent> {
const enabledTools = getEnabledChatTools(params);
const maxTokens = await getMessagesMaxTokens(params);
if (!enabledTools.length) {
const rawResponses: unknown[] = [];
const usageAcc: Required<ToolAwareUsage> = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
@@ -300,7 +259,7 @@ export async function* streamWithMessagesApi(params: ToolAwareCompletionParams):
const stream = await params.client.messages.create({
model: params.model,
system: buildTopLevelSystemPrompt(params.messages, params.userLocation),
max_tokens: maxTokens,
max_tokens: params.maxTokens ?? 1024,
temperature: params.temperature,
messages: buildBaseMessages(params),
stream: true,
@@ -356,7 +315,7 @@ export async function* streamWithMessagesApi(params: ToolAwareCompletionParams):
const stream = await params.client.messages.create({
model: params.model,
system: buildTopLevelSystemPrompt(params.messages, params.userLocation, buildChatToolSystemPrompt(params)),
max_tokens: maxTokens,
max_tokens: params.maxTokens ?? 1024,
temperature: params.temperature,
messages: conversation,
tools: toTools(enabledTools),
-88
View File
@@ -140,94 +140,6 @@ test("plain Chat Completions stream does not send Sybil-managed tools", async ()
assert.equal(events.at(-1)?.type === "done" ? events.at(-1)?.result.text : null, "Hi");
});
test("Messages API defaults max_tokens to the Anthropic model maximum", async () => {
let requestBody: any = null;
let retrievedModel: string | null = null;
const client = {
models: {
retrieve: async (model: string) => {
retrievedModel = model;
return { id: model, max_tokens: 128000 };
},
},
messages: {
create: async (body: any) => {
requestBody = body;
return {
content: [{ type: "text", text: "Done" }],
usage: { input_tokens: 1, output_tokens: 1 },
};
},
},
};
const result = await completeWithMessagesApi({
client: client as any,
model: "claude-max-default-test",
messages: [{ role: "user", content: "Say done" }],
});
assert.equal(retrievedModel, "claude-max-default-test");
assert.equal(requestBody?.max_tokens, 128000);
assert.equal(result.text, "Done");
});
test("Messages API preserves explicit maxTokens", async () => {
let requestBody: any = null;
let didRetrieveModel = false;
const client = {
models: {
retrieve: async () => {
didRetrieveModel = true;
return { max_tokens: 128000 };
},
},
messages: {
create: async (body: any) => {
requestBody = body;
return streamFrom([
{
type: "message_start",
message: {
usage: { input_tokens: 1, output_tokens: 0 },
},
},
{
type: "content_block_start",
index: 0,
content_block: { type: "text", text: "" },
},
{
type: "content_block_delta",
index: 0,
delta: { type: "text_delta", text: "Done" },
},
{ type: "content_block_stop", index: 0 },
{
type: "message_delta",
delta: { stop_reason: "end_turn", stop_sequence: null },
usage: { output_tokens: 1 },
},
{ type: "message_stop" },
]);
},
},
};
const events = await collectEvents(
streamWithMessagesApi({
client: client as any,
model: "claude-explicit-max-test",
messages: [{ role: "user", content: "Say done" }],
maxTokens: 4096,
})
);
assert.equal(didRetrieveModel, false);
assert.equal(requestBody?.max_tokens, 4096);
assert.equal(events.at(-1)?.type === "done" ? events.at(-1)?.result.text : null, "Done");
});
test("fetch_url sends browser-like navigation headers", async () => {
const originalFetch = globalThis.fetch;
const fetchCalls: Array<{ input: RequestInfo | URL; init?: RequestInit }> = [];
+1 -30
View File
@@ -286,14 +286,6 @@ textarea {
word-break: break-word;
}
.md-content > :first-child {
margin-top: 0;
}
.md-content > :last-child {
margin-bottom: 0;
}
.md-table-scroll {
max-width: 100%;
margin: 0.35rem 0 1rem;
@@ -392,8 +384,7 @@ textarea {
.md-content ul,
.md-content ol {
margin-top: 0.85rem;
margin-bottom: 0.85rem;
margin-top: 0.65rem;
margin-left: 0;
padding-left: 0;
list-style: none;
@@ -405,26 +396,6 @@ textarea {
padding-left: 1.35rem;
}
.md-content ul > li {
position: relative;
padding-left: 1.1rem;
}
.md-content ul > li::before {
content: "";
position: absolute;
left: 0;
top: 0.76em;
width: 0.36rem;
height: 0.36rem;
border-radius: 9999px;
background: hsl(188 86% 62%);
box-shadow:
0 0 0 2px hsl(188 86% 62% / 0.12),
0 0 10px hsl(188 86% 62% / 0.42);
transform: translateY(-50%);
}
.md-content li + li {
margin-top: 0.3rem;
}