Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .github/workflows/canary.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ jobs:
cache: npm
- name: Install dependencies from the unpublished SDK source
run: |
npm ci --prefix .ci/inkbox/sdk/typescript
bash "$GITHUB_WORKSPACE/tests/ci/npm_with_retry.sh" ci --prefix .ci/inkbox/sdk/typescript
npm run build --prefix .ci/inkbox/sdk/typescript
npm install --no-save --package-lock=false \
bash "$GITHUB_WORKSPACE/tests/ci/npm_with_retry.sh" install --no-save --package-lock=false \
./.ci/inkbox/sdk/typescript \
@opencode-ai/sdk@latest @opencode-ai/plugin@latest
- run: npm run lint
Expand All @@ -48,12 +48,12 @@ jobs:
cache: npm
- name: Install dependencies from the unpublished SDK source
run: |
npm ci --prefix .ci/inkbox/sdk/typescript
bash "$GITHUB_WORKSPACE/tests/ci/npm_with_retry.sh" ci --prefix .ci/inkbox/sdk/typescript
npm run build --prefix .ci/inkbox/sdk/typescript
npm install --no-save --package-lock=false \
bash "$GITHUB_WORKSPACE/tests/ci/npm_with_retry.sh" install --no-save --package-lock=false \
./.ci/inkbox/sdk/typescript \
@opencode-ai/sdk@1.17.18 @opencode-ai/plugin@1.17.18
- run: npm install -g opencode-ai@latest
- run: bash "$GITHUB_WORKSPACE/tests/ci/npm_with_retry.sh" install -g opencode-ai@latest
- run: bash scripts/smoke-loader.sh

notify:
Expand Down
12 changes: 6 additions & 6 deletions .github/workflows/live-a2a.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,11 @@ jobs:

- name: Install plugin and host
run: |
npm ci
npm install --no-save --package-lock=false \
bash "$GITHUB_WORKSPACE/tests/ci/npm_with_retry.sh" ci
bash "$GITHUB_WORKSPACE/tests/ci/npm_with_retry.sh" install --no-save --package-lock=false \
@inkbox/sdk@0.5.9 \
@opencode-ai/sdk@1.17.18 @opencode-ai/plugin@1.17.18
npm install -g opencode-ai@latest
bash "$GITHUB_WORKSPACE/tests/ci/npm_with_retry.sh" install -g opencode-ai@latest

- name: Boot the AUT gateway
env:
Expand All @@ -87,11 +87,11 @@ jobs:
INKBOX_BASE_URL: ${{ vars.INKBOX_BASE_URL || 'https://inkbox.ai' }}
run: python3 tests/live/a2a_driver.py

- name: Dump gateway logs on failure
- name: Report content-free failure state
if: failure()
run: |
tail -n 300 "$AUT_GATEWAY_LOG" 2>/dev/null || true
tail -n 100 "$AUT_SERVE_LOG" 2>/dev/null || true
test -s "$AUT_GATEWAY_LOG" && echo "gateway_log=present" || echo "gateway_log=empty"
test -s "$AUT_SERVE_LOG" && echo "serve_log=present" || echo "serve_log=empty"

- name: Stop gateway
if: always()
Expand Down
30 changes: 8 additions & 22 deletions .github/workflows/live-channels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,12 @@ jobs:
cache: npm
- name: Install dependencies from the unpublished SDK source
run: |
npm ci --prefix .ci/inkbox/sdk/typescript
bash "$GITHUB_WORKSPACE/tests/ci/npm_with_retry.sh" ci --prefix .ci/inkbox/sdk/typescript
npm run build --prefix .ci/inkbox/sdk/typescript
npm install --no-save --package-lock=false \
bash "$GITHUB_WORKSPACE/tests/ci/npm_with_retry.sh" install --no-save --package-lock=false \
./.ci/inkbox/sdk/typescript \
@opencode-ai/sdk@1.17.18 @opencode-ai/plugin@1.17.18
- run: npm install -g opencode-ai@latest
- run: bash "$GITHUB_WORKSPACE/tests/ci/npm_with_retry.sh" install -g opencode-ai@latest

- name: Start mock model
if: matrix.mode == 'mock'
Expand All @@ -77,7 +77,7 @@ jobs:
http://127.0.0.1:8088/v1/models >/dev/null && { echo "mock model ready"; exit 0; }
sleep 1
done
echo "::error::mock model did not start"; cat "$RUNNER_TEMP/mock.log"; exit 1
echo "::error::mock model did not start"; exit 1

- name: Boot the AUT gateway (${{ matrix.mode }})
env:
Expand All @@ -101,30 +101,16 @@ jobs:
npx vitest run --config vitest.live.config.ts \
tests/live/email-reply.test.ts tests/live/sms.test.ts

# Failure-only: these logs carry live phone/email content and this
# repo's Action logs are public.
- name: Dump logs (on failure only)
- name: Report content-free failure state
if: failure() || cancelled()
run: |
echo "=== gateway.log ==="; cat "$AUT_GATEWAY_LOG" || true
echo "=== serve.log ==="; tail -n 100 "$AUT_SERVE_LOG" || true
echo "=== mock.log ==="; cat "$RUNNER_TEMP/mock.log" 2>/dev/null || true
test -s "$AUT_GATEWAY_LOG" && echo "gateway_log=present" || echo "gateway_log=empty"
test -s "$AUT_SERVE_LOG" && echo "serve_log=present" || echo "serve_log=empty"
test -s "$RUNNER_TEMP/mock.log" && echo "mock_log=present" || echo "mock_log=empty"

- name: Tear down (always)
if: always()
run: |
kill "$AUT_GATEWAY_PID" 2>/dev/null || true
kill "$AUT_SERVE_PID" 2>/dev/null || true
kill "$(cat "$RUNNER_TEMP/mock.pid" 2>/dev/null)" 2>/dev/null || true

- name: Upload artifacts (on failure only)
if: failure() || cancelled()
uses: actions/upload-artifact@v7
with:
name: live-logs-${{ matrix.mode }}
retention-days: 5
path: |
${{ env.AUT_GATEWAY_LOG }}
${{ env.AUT_SERVE_LOG }}
${{ runner.temp }}/mock.log
if-no-files-found: ignore
23 changes: 6 additions & 17 deletions .github/workflows/live-external-events.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,12 @@ jobs:
cache: npm
- name: Install dependencies from the unpublished SDK source
run: |
npm ci --prefix .ci/inkbox/sdk/typescript
bash "$GITHUB_WORKSPACE/tests/ci/npm_with_retry.sh" ci --prefix .ci/inkbox/sdk/typescript
npm run build --prefix .ci/inkbox/sdk/typescript
npm install --no-save --package-lock=false \
bash "$GITHUB_WORKSPACE/tests/ci/npm_with_retry.sh" install --no-save --package-lock=false \
./.ci/inkbox/sdk/typescript \
@opencode-ai/sdk@1.17.18 @opencode-ai/plugin@1.17.18
- run: npm install -g opencode-ai@latest
- run: bash "$GITHUB_WORKSPACE/tests/ci/npm_with_retry.sh" install -g opencode-ai@latest

- name: Boot the AUT gateway (real model, external events on)
env:
Expand All @@ -79,25 +79,14 @@ jobs:
run: |
npx vitest run --config vitest.live.config.ts tests/live/external-event.test.ts

- name: Dump logs (on failure only)
- name: Report content-free failure state
if: failure()
run: |
echo "=== gateway.log ==="; cat "$AUT_GATEWAY_LOG" || true
echo "=== serve.log ==="; tail -n 100 "$AUT_SERVE_LOG" || true
test -s "$AUT_GATEWAY_LOG" && echo "gateway_log=present" || echo "gateway_log=empty"
test -s "$AUT_SERVE_LOG" && echo "serve_log=present" || echo "serve_log=empty"

- name: Tear down (always)
if: always()
run: |
kill "$AUT_GATEWAY_PID" 2>/dev/null || true
kill "$AUT_SERVE_PID" 2>/dev/null || true

- name: Upload artifacts (on failure only)
if: failure()
uses: actions/upload-artifact@v7
with:
name: external-events-logs
retention-days: 5
path: |
${{ env.AUT_GATEWAY_LOG }}
${{ env.AUT_SERVE_LOG }}
if-no-files-found: ignore
33 changes: 10 additions & 23 deletions .github/workflows/live-voice.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,12 @@ jobs:
cache: npm
- name: Install dependencies from the unpublished SDK source
run: |
npm ci --prefix .ci/inkbox/sdk/typescript
bash "$GITHUB_WORKSPACE/tests/ci/npm_with_retry.sh" ci --prefix .ci/inkbox/sdk/typescript
npm run build --prefix .ci/inkbox/sdk/typescript
npm install --no-save --package-lock=false \
bash "$GITHUB_WORKSPACE/tests/ci/npm_with_retry.sh" install --no-save --package-lock=false \
./.ci/inkbox/sdk/typescript \
@opencode-ai/sdk@1.17.18 @opencode-ai/plugin@1.17.18
- run: npm install -g opencode-ai@latest
- run: bash "$GITHUB_WORKSPACE/tests/ci/npm_with_retry.sh" install -g opencode-ai@latest

- name: Boot the AUT gateway (voice; ${{ matrix.scenario }})
env:
Expand All @@ -99,17 +99,17 @@ jobs:
if [ "${{ matrix.scenario }}" = "outbound_hosted" ]; then
HOSTED_MARKER="$(node scripts/nato-marker.mjs "$GITHUB_RUN_ID" "$GITHUB_RUN_ATTEMPT")"
echo "HOSTED_POST_CALL_MARKER=$HOSTED_MARKER" >> "$GITHUB_ENV"
export VOICE_DRIVER_LINE="After we hang up, send me one SMS. Create one post-call action now with the title Send SMS and put this exact five-word SMS body in the action details: $HOSTED_MARKER. Wait for the action tool to succeed, then read all five words back to me. Do not paraphrase, omit a word, or send the SMS during the call."
export VOICE_DRIVER_LINE="After we hang up, send me one SMS containing exactly these three words: $HOSTED_MARKER. Create one post-call action now. Set both the action title and the action details to this exact five-word phrase: Send SMS $HOSTED_MARKER. Wait for the action tool to succeed, then read the exact three-word SMS body back to me. Do not paraphrase, omit a word, or send the SMS during the call."
export VOICE_DRIVER_LISTEN=180
export VOICE_DRIVER_AUTO_STOP=false
fi
nohup node tests/live/voice-driver.mjs > "$RUNNER_TEMP/driver.log" 2>&1 &
echo $! > "$RUNNER_TEMP/driver.pid"
for _ in $(seq 1 30); do # up to ~90s
[ -s "$VOICE_DRIVER_STATE" ] && { echo "driver ready:"; cat "$VOICE_DRIVER_STATE"; exit 0; }
[ -s "$VOICE_DRIVER_STATE" ] && { echo "driver ready"; exit 0; }
sleep 3
done
echo "::error::driver did not become ready"; cat "$RUNNER_TEMP/driver.log"; exit 1
echo "::error::driver did not become ready"; exit 1

- name: Run voice test (${{ matrix.scenario }})
env:
Expand All @@ -123,13 +123,12 @@ jobs:
run: |
npx vitest run --config vitest.live.config.ts tests/live/voice.test.ts

# Failure-only: these logs carry live call content and this repo is public.
- name: Dump logs (on failure only)
- name: Report content-free failure state
if: failure() || cancelled()
run: |
echo "=== gateway.log ==="; cat "$AUT_GATEWAY_LOG" || true
echo "=== serve.log ==="; tail -n 100 "$AUT_SERVE_LOG" || true
echo "=== driver.log ==="; cat "$RUNNER_TEMP/driver.log" || true
test -s "$AUT_GATEWAY_LOG" && echo "gateway_log=present" || echo "gateway_log=empty"
test -s "$AUT_SERVE_LOG" && echo "serve_log=present" || echo "serve_log=empty"
test -s "$RUNNER_TEMP/driver.log" && echo "driver_log=present" || echo "driver_log=empty"

- name: Tear down (always)
if: always()
Expand All @@ -138,15 +137,3 @@ jobs:
kill "$AUT_GATEWAY_PID" 2>/dev/null || true
kill "$AUT_SERVE_PID" 2>/dev/null || true
sleep 3 # let the driver revert its number on exit

- name: Upload artifacts (on failure only)
if: failure() || cancelled()
uses: actions/upload-artifact@v7
with:
name: voice-logs-${{ matrix.scenario }}
retention-days: 5
path: |
${{ env.AUT_GATEWAY_LOG }}
${{ env.AUT_SERVE_LOG }}
${{ runner.temp }}/driver.log
if-no-files-found: ignore
20 changes: 12 additions & 8 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,19 @@ jobs:
with:
node-version: 22
cache: npm
- uses: actions/setup-python@v6
with:
python-version: "3.12"
- run: |
npm ci --prefix .ci/inkbox/sdk/typescript
bash "$GITHUB_WORKSPACE/tests/ci/npm_with_retry.sh" ci --prefix .ci/inkbox/sdk/typescript
npm run build --prefix .ci/inkbox/sdk/typescript
npm install --no-save --package-lock=false \
bash "$GITHUB_WORKSPACE/tests/ci/npm_with_retry.sh" install --no-save --package-lock=false \
./.ci/inkbox/sdk/typescript \
@opencode-ai/sdk@1.17.18 @opencode-ai/plugin@1.17.18
- run: npm run lint
- run: npm run typecheck
- run: npm test
- run: python3 -m unittest tests/live/test_a2a_preflight.py
- run: npm run build

# Same contract suite canary runs on a schedule, but here it gates PRs so a
Expand All @@ -46,13 +50,13 @@ jobs:
node-version: 22
cache: npm
- run: |
npm ci --prefix .ci/inkbox/sdk/typescript
bash "$GITHUB_WORKSPACE/tests/ci/npm_with_retry.sh" ci --prefix .ci/inkbox/sdk/typescript
npm run build --prefix .ci/inkbox/sdk/typescript
npm install --no-save --package-lock=false \
bash "$GITHUB_WORKSPACE/tests/ci/npm_with_retry.sh" install --no-save --package-lock=false \
./.ci/inkbox/sdk/typescript \
@opencode-ai/sdk@1.17.18 @opencode-ai/plugin@1.17.18
- run: |
npm install --no-save --package-lock=false \
bash "$GITHUB_WORKSPACE/tests/ci/npm_with_retry.sh" install --no-save --package-lock=false \
./.ci/inkbox/sdk/typescript \
@opencode-ai/sdk@latest @opencode-ai/plugin@latest
- run: npm run typecheck
Expand All @@ -72,10 +76,10 @@ jobs:
node-version: 22
cache: npm
- run: |
npm ci --prefix .ci/inkbox/sdk/typescript
bash "$GITHUB_WORKSPACE/tests/ci/npm_with_retry.sh" ci --prefix .ci/inkbox/sdk/typescript
npm run build --prefix .ci/inkbox/sdk/typescript
npm install --no-save --package-lock=false \
bash "$GITHUB_WORKSPACE/tests/ci/npm_with_retry.sh" install --no-save --package-lock=false \
./.ci/inkbox/sdk/typescript \
@opencode-ai/sdk@1.17.18 @opencode-ai/plugin@1.17.18
- run: npm install -g opencode-ai@latest
- run: bash "$GITHUB_WORKSPACE/tests/ci/npm_with_retry.sh" install -g opencode-ai@latest
- run: INKBOX_SDK_PATH="$PWD/.ci/inkbox/sdk/typescript" bash scripts/smoke-loader.sh
65 changes: 30 additions & 35 deletions scripts/nato-marker.mjs
Original file line number Diff line number Diff line change
@@ -1,44 +1,39 @@
import { createHash } from "node:crypto";
import { pathToFileURL } from "node:url";

const RADIO_WORDS = [
"alpha",
"bravo",
"charlie",
"delta",
"echo",
"foxtrot",
"golf",
"hotel",
"india",
"juliet",
"kilo",
"lima",
"mike",
"november",
"oscar",
"papa",
"quebec",
"romeo",
"sierra",
"tango",
"uniform",
"victor",
"whiskey",
"xray",
"yankee",
"zulu",
const SPEECH_WORDS = [
"banana",
"elephant",
"pineapple",
"alligator",
"motorcycle",
"umbrella",
"dinosaur",
"potato",
"computer",
"volcano",
"airplane",
"butterfly",
"kangaroo",
"octopus",
"calendar",
"chocolate",
"hospital",
"library",
"sandwich",
"telescope",
];

export function natoMarker(runId, runAttempt) {
let value = BigInt(runId) * 10n + BigInt(runAttempt);
const used = new Set();
const token = `${runId}-${runAttempt}`;
if (!/^\d+-\d+$/.test(token)) throw new Error("run id and attempt must be numeric");
let value = BigInt(`0x${createHash("sha256").update(token).digest("hex")}`);
const available = [...SPEECH_WORDS];
const marker = [];
for (let count = 0; count < 5; count += 1) {
let index = Number(value % BigInt(RADIO_WORDS.length));
value /= BigInt(RADIO_WORDS.length);
while (used.has(index)) index = (index + 1) % RADIO_WORDS.length;
used.add(index);
marker.push(RADIO_WORDS[index]);
for (let count = 0; count < 3; count += 1) {
const index = Number(value % BigInt(available.length));
value /= BigInt(available.length);
marker.push(available.splice(index, 1)[0]);
}
return marker.join(" ");
}
Expand Down
11 changes: 10 additions & 1 deletion src/gateway/dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { ContactResolver } from "./contacts.js";
import { normalizeAddress } from "./contacts.js";
import type { NotifyOnce } from "./dedup.js";
import { deliveryFailureKey, deliveryFailureRecovery } from "./delivery-policy.js";
import { isSuccessfulHostedSmsMessage } from "./hosted-call-registry.js";
import { downloadMedia, mediaDir } from "./media.js";
import { SILENT } from "./prompts.js";
import type {
Expand Down Expand Up @@ -50,7 +51,11 @@ export interface DispatchDeps {
// may retry); filtered/ignored events return true (ack, no retry).
export async function dispatchEvent(deps: DispatchDeps, event: VerifiedEvent): Promise<boolean> {
if (event.provider !== "inkbox") {
if (deps.onExternal) await deps.onExternal(event);
if (deps.onExternal) {
void deps
.onExternal(event)
.catch((error) => deps.logger.error("external.dispatch_failed", { error: String(error) }));
}
return true;
}
const type = event.eventType ?? inferType(event.body);
Expand Down Expand Up @@ -421,6 +426,10 @@ async function handleDeliveryFailure(
const r = resourceOf(event.body, isText ? "text_message" : "message");
if (str(r?.direction)?.toLowerCase() === "inbound") return true;
const messageId = str(r?.id);
if (isText && messageId && isSuccessfulHostedSmsMessage(messageId)) {
deps.logger.info("dispatch.hosted_sms_delivery_failed");
return true;
}
const recipientRows = Array.isArray(r?.recipients) ? r.recipients : [];
const failedRecipient = recipientRows
.map((item) => record(item))
Expand Down
Loading