Skip to content

Commit 7de97ee

Browse files
authored
Merge pull request #24 from inkbox-ai/fix/live-ci-stability
Stabilize live plugin validation
2 parents 641c8c9 + 02de90d commit 7de97ee

34 files changed

Lines changed: 949 additions & 386 deletions

.github/workflows/canary.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ jobs:
2323
cache: npm
2424
- name: Install dependencies from the unpublished SDK source
2525
run: |
26-
npm ci --prefix .ci/inkbox/sdk/typescript
26+
bash "$GITHUB_WORKSPACE/tests/ci/npm_with_retry.sh" ci --prefix .ci/inkbox/sdk/typescript
2727
npm run build --prefix .ci/inkbox/sdk/typescript
28-
npm install --no-save --package-lock=false \
28+
bash "$GITHUB_WORKSPACE/tests/ci/npm_with_retry.sh" install --no-save --package-lock=false \
2929
./.ci/inkbox/sdk/typescript \
3030
@opencode-ai/sdk@latest @opencode-ai/plugin@latest
3131
- run: npm run lint
@@ -48,12 +48,12 @@ jobs:
4848
cache: npm
4949
- name: Install dependencies from the unpublished SDK source
5050
run: |
51-
npm ci --prefix .ci/inkbox/sdk/typescript
51+
bash "$GITHUB_WORKSPACE/tests/ci/npm_with_retry.sh" ci --prefix .ci/inkbox/sdk/typescript
5252
npm run build --prefix .ci/inkbox/sdk/typescript
53-
npm install --no-save --package-lock=false \
53+
bash "$GITHUB_WORKSPACE/tests/ci/npm_with_retry.sh" install --no-save --package-lock=false \
5454
./.ci/inkbox/sdk/typescript \
5555
@opencode-ai/sdk@1.17.18 @opencode-ai/plugin@1.17.18
56-
- run: npm install -g opencode-ai@latest
56+
- run: bash "$GITHUB_WORKSPACE/tests/ci/npm_with_retry.sh" install -g opencode-ai@latest
5757
- run: bash scripts/smoke-loader.sh
5858

5959
notify:

.github/workflows/live-a2a.yml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,11 @@ jobs:
6262

6363
- name: Install plugin and host
6464
run: |
65-
npm ci
66-
npm install --no-save --package-lock=false \
65+
bash "$GITHUB_WORKSPACE/tests/ci/npm_with_retry.sh" ci
66+
bash "$GITHUB_WORKSPACE/tests/ci/npm_with_retry.sh" install --no-save --package-lock=false \
6767
@inkbox/sdk@0.5.9 \
6868
@opencode-ai/sdk@1.17.18 @opencode-ai/plugin@1.17.18
69-
npm install -g opencode-ai@latest
69+
bash "$GITHUB_WORKSPACE/tests/ci/npm_with_retry.sh" install -g opencode-ai@latest
7070
7171
- name: Boot the AUT gateway
7272
env:
@@ -87,11 +87,11 @@ jobs:
8787
INKBOX_BASE_URL: ${{ vars.INKBOX_BASE_URL || 'https://inkbox.ai' }}
8888
run: python3 tests/live/a2a_driver.py
8989

90-
- name: Dump gateway logs on failure
90+
- name: Report content-free failure state
9191
if: failure()
9292
run: |
93-
tail -n 300 "$AUT_GATEWAY_LOG" 2>/dev/null || true
94-
tail -n 100 "$AUT_SERVE_LOG" 2>/dev/null || true
93+
test -s "$AUT_GATEWAY_LOG" && echo "gateway_log=present" || echo "gateway_log=empty"
94+
test -s "$AUT_SERVE_LOG" && echo "serve_log=present" || echo "serve_log=empty"
9595
9696
- name: Stop gateway
9797
if: always()

.github/workflows/live-channels.yml

Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,12 @@ jobs:
6060
cache: npm
6161
- name: Install dependencies from the unpublished SDK source
6262
run: |
63-
npm ci --prefix .ci/inkbox/sdk/typescript
63+
bash "$GITHUB_WORKSPACE/tests/ci/npm_with_retry.sh" ci --prefix .ci/inkbox/sdk/typescript
6464
npm run build --prefix .ci/inkbox/sdk/typescript
65-
npm install --no-save --package-lock=false \
65+
bash "$GITHUB_WORKSPACE/tests/ci/npm_with_retry.sh" install --no-save --package-lock=false \
6666
./.ci/inkbox/sdk/typescript \
6767
@opencode-ai/sdk@1.17.18 @opencode-ai/plugin@1.17.18
68-
- run: npm install -g opencode-ai@latest
68+
- run: bash "$GITHUB_WORKSPACE/tests/ci/npm_with_retry.sh" install -g opencode-ai@latest
6969

7070
- name: Start mock model
7171
if: matrix.mode == 'mock'
@@ -77,7 +77,7 @@ jobs:
7777
http://127.0.0.1:8088/v1/models >/dev/null && { echo "mock model ready"; exit 0; }
7878
sleep 1
7979
done
80-
echo "::error::mock model did not start"; cat "$RUNNER_TEMP/mock.log"; exit 1
80+
echo "::error::mock model did not start"; exit 1
8181
8282
- name: Boot the AUT gateway (${{ matrix.mode }})
8383
env:
@@ -101,30 +101,16 @@ jobs:
101101
npx vitest run --config vitest.live.config.ts \
102102
tests/live/email-reply.test.ts tests/live/sms.test.ts
103103
104-
# Failure-only: these logs carry live phone/email content and this
105-
# repo's Action logs are public.
106-
- name: Dump logs (on failure only)
104+
- name: Report content-free failure state
107105
if: failure() || cancelled()
108106
run: |
109-
echo "=== gateway.log ==="; cat "$AUT_GATEWAY_LOG" || true
110-
echo "=== serve.log ==="; tail -n 100 "$AUT_SERVE_LOG" || true
111-
echo "=== mock.log ==="; cat "$RUNNER_TEMP/mock.log" 2>/dev/null || true
107+
test -s "$AUT_GATEWAY_LOG" && echo "gateway_log=present" || echo "gateway_log=empty"
108+
test -s "$AUT_SERVE_LOG" && echo "serve_log=present" || echo "serve_log=empty"
109+
test -s "$RUNNER_TEMP/mock.log" && echo "mock_log=present" || echo "mock_log=empty"
112110
113111
- name: Tear down (always)
114112
if: always()
115113
run: |
116114
kill "$AUT_GATEWAY_PID" 2>/dev/null || true
117115
kill "$AUT_SERVE_PID" 2>/dev/null || true
118116
kill "$(cat "$RUNNER_TEMP/mock.pid" 2>/dev/null)" 2>/dev/null || true
119-
120-
- name: Upload artifacts (on failure only)
121-
if: failure() || cancelled()
122-
uses: actions/upload-artifact@v7
123-
with:
124-
name: live-logs-${{ matrix.mode }}
125-
retention-days: 5
126-
path: |
127-
${{ env.AUT_GATEWAY_LOG }}
128-
${{ env.AUT_SERVE_LOG }}
129-
${{ runner.temp }}/mock.log
130-
if-no-files-found: ignore

.github/workflows/live-external-events.yml

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,12 @@ jobs:
4949
cache: npm
5050
- name: Install dependencies from the unpublished SDK source
5151
run: |
52-
npm ci --prefix .ci/inkbox/sdk/typescript
52+
bash "$GITHUB_WORKSPACE/tests/ci/npm_with_retry.sh" ci --prefix .ci/inkbox/sdk/typescript
5353
npm run build --prefix .ci/inkbox/sdk/typescript
54-
npm install --no-save --package-lock=false \
54+
bash "$GITHUB_WORKSPACE/tests/ci/npm_with_retry.sh" install --no-save --package-lock=false \
5555
./.ci/inkbox/sdk/typescript \
5656
@opencode-ai/sdk@1.17.18 @opencode-ai/plugin@1.17.18
57-
- run: npm install -g opencode-ai@latest
57+
- run: bash "$GITHUB_WORKSPACE/tests/ci/npm_with_retry.sh" install -g opencode-ai@latest
5858

5959
- name: Boot the AUT gateway (real model, external events on)
6060
env:
@@ -79,25 +79,14 @@ jobs:
7979
run: |
8080
npx vitest run --config vitest.live.config.ts tests/live/external-event.test.ts
8181
82-
- name: Dump logs (on failure only)
82+
- name: Report content-free failure state
8383
if: failure()
8484
run: |
85-
echo "=== gateway.log ==="; cat "$AUT_GATEWAY_LOG" || true
86-
echo "=== serve.log ==="; tail -n 100 "$AUT_SERVE_LOG" || true
85+
test -s "$AUT_GATEWAY_LOG" && echo "gateway_log=present" || echo "gateway_log=empty"
86+
test -s "$AUT_SERVE_LOG" && echo "serve_log=present" || echo "serve_log=empty"
8787
8888
- name: Tear down (always)
8989
if: always()
9090
run: |
9191
kill "$AUT_GATEWAY_PID" 2>/dev/null || true
9292
kill "$AUT_SERVE_PID" 2>/dev/null || true
93-
94-
- name: Upload artifacts (on failure only)
95-
if: failure()
96-
uses: actions/upload-artifact@v7
97-
with:
98-
name: external-events-logs
99-
retention-days: 5
100-
path: |
101-
${{ env.AUT_GATEWAY_LOG }}
102-
${{ env.AUT_SERVE_LOG }}
103-
if-no-files-found: ignore

.github/workflows/live-voice.yml

Lines changed: 10 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,12 @@ jobs:
6868
cache: npm
6969
- name: Install dependencies from the unpublished SDK source
7070
run: |
71-
npm ci --prefix .ci/inkbox/sdk/typescript
71+
bash "$GITHUB_WORKSPACE/tests/ci/npm_with_retry.sh" ci --prefix .ci/inkbox/sdk/typescript
7272
npm run build --prefix .ci/inkbox/sdk/typescript
73-
npm install --no-save --package-lock=false \
73+
bash "$GITHUB_WORKSPACE/tests/ci/npm_with_retry.sh" install --no-save --package-lock=false \
7474
./.ci/inkbox/sdk/typescript \
7575
@opencode-ai/sdk@1.17.18 @opencode-ai/plugin@1.17.18
76-
- run: npm install -g opencode-ai@latest
76+
- run: bash "$GITHUB_WORKSPACE/tests/ci/npm_with_retry.sh" install -g opencode-ai@latest
7777

7878
- name: Boot the AUT gateway (voice; ${{ matrix.scenario }})
7979
env:
@@ -99,17 +99,17 @@ jobs:
9999
if [ "${{ matrix.scenario }}" = "outbound_hosted" ]; then
100100
HOSTED_MARKER="$(node scripts/nato-marker.mjs "$GITHUB_RUN_ID" "$GITHUB_RUN_ATTEMPT")"
101101
echo "HOSTED_POST_CALL_MARKER=$HOSTED_MARKER" >> "$GITHUB_ENV"
102-
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."
102+
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."
103103
export VOICE_DRIVER_LISTEN=180
104104
export VOICE_DRIVER_AUTO_STOP=false
105105
fi
106106
nohup node tests/live/voice-driver.mjs > "$RUNNER_TEMP/driver.log" 2>&1 &
107107
echo $! > "$RUNNER_TEMP/driver.pid"
108108
for _ in $(seq 1 30); do # up to ~90s
109-
[ -s "$VOICE_DRIVER_STATE" ] && { echo "driver ready:"; cat "$VOICE_DRIVER_STATE"; exit 0; }
109+
[ -s "$VOICE_DRIVER_STATE" ] && { echo "driver ready"; exit 0; }
110110
sleep 3
111111
done
112-
echo "::error::driver did not become ready"; cat "$RUNNER_TEMP/driver.log"; exit 1
112+
echo "::error::driver did not become ready"; exit 1
113113
114114
- name: Run voice test (${{ matrix.scenario }})
115115
env:
@@ -123,13 +123,12 @@ jobs:
123123
run: |
124124
npx vitest run --config vitest.live.config.ts tests/live/voice.test.ts
125125
126-
# Failure-only: these logs carry live call content and this repo is public.
127-
- name: Dump logs (on failure only)
126+
- name: Report content-free failure state
128127
if: failure() || cancelled()
129128
run: |
130-
echo "=== gateway.log ==="; cat "$AUT_GATEWAY_LOG" || true
131-
echo "=== serve.log ==="; tail -n 100 "$AUT_SERVE_LOG" || true
132-
echo "=== driver.log ==="; cat "$RUNNER_TEMP/driver.log" || true
129+
test -s "$AUT_GATEWAY_LOG" && echo "gateway_log=present" || echo "gateway_log=empty"
130+
test -s "$AUT_SERVE_LOG" && echo "serve_log=present" || echo "serve_log=empty"
131+
test -s "$RUNNER_TEMP/driver.log" && echo "driver_log=present" || echo "driver_log=empty"
133132
134133
- name: Tear down (always)
135134
if: always()
@@ -138,15 +137,3 @@ jobs:
138137
kill "$AUT_GATEWAY_PID" 2>/dev/null || true
139138
kill "$AUT_SERVE_PID" 2>/dev/null || true
140139
sleep 3 # let the driver revert its number on exit
141-
142-
- name: Upload artifacts (on failure only)
143-
if: failure() || cancelled()
144-
uses: actions/upload-artifact@v7
145-
with:
146-
name: voice-logs-${{ matrix.scenario }}
147-
retention-days: 5
148-
path: |
149-
${{ env.AUT_GATEWAY_LOG }}
150-
${{ env.AUT_SERVE_LOG }}
151-
${{ runner.temp }}/driver.log
152-
if-no-files-found: ignore

.github/workflows/tests.yml

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,19 @@ jobs:
1919
with:
2020
node-version: 22
2121
cache: npm
22+
- uses: actions/setup-python@v6
23+
with:
24+
python-version: "3.12"
2225
- run: |
23-
npm ci --prefix .ci/inkbox/sdk/typescript
26+
bash "$GITHUB_WORKSPACE/tests/ci/npm_with_retry.sh" ci --prefix .ci/inkbox/sdk/typescript
2427
npm run build --prefix .ci/inkbox/sdk/typescript
25-
npm install --no-save --package-lock=false \
28+
bash "$GITHUB_WORKSPACE/tests/ci/npm_with_retry.sh" install --no-save --package-lock=false \
2629
./.ci/inkbox/sdk/typescript \
2730
@opencode-ai/sdk@1.17.18 @opencode-ai/plugin@1.17.18
2831
- run: npm run lint
2932
- run: npm run typecheck
3033
- run: npm test
34+
- run: python3 -m unittest tests/live/test_a2a_preflight.py
3135
- run: npm run build
3236

3337
# Same contract suite canary runs on a schedule, but here it gates PRs so a
@@ -46,13 +50,13 @@ jobs:
4650
node-version: 22
4751
cache: npm
4852
- run: |
49-
npm ci --prefix .ci/inkbox/sdk/typescript
53+
bash "$GITHUB_WORKSPACE/tests/ci/npm_with_retry.sh" ci --prefix .ci/inkbox/sdk/typescript
5054
npm run build --prefix .ci/inkbox/sdk/typescript
51-
npm install --no-save --package-lock=false \
55+
bash "$GITHUB_WORKSPACE/tests/ci/npm_with_retry.sh" install --no-save --package-lock=false \
5256
./.ci/inkbox/sdk/typescript \
5357
@opencode-ai/sdk@1.17.18 @opencode-ai/plugin@1.17.18
5458
- run: |
55-
npm install --no-save --package-lock=false \
59+
bash "$GITHUB_WORKSPACE/tests/ci/npm_with_retry.sh" install --no-save --package-lock=false \
5660
./.ci/inkbox/sdk/typescript \
5761
@opencode-ai/sdk@latest @opencode-ai/plugin@latest
5862
- run: npm run typecheck
@@ -72,10 +76,10 @@ jobs:
7276
node-version: 22
7377
cache: npm
7478
- run: |
75-
npm ci --prefix .ci/inkbox/sdk/typescript
79+
bash "$GITHUB_WORKSPACE/tests/ci/npm_with_retry.sh" ci --prefix .ci/inkbox/sdk/typescript
7680
npm run build --prefix .ci/inkbox/sdk/typescript
77-
npm install --no-save --package-lock=false \
81+
bash "$GITHUB_WORKSPACE/tests/ci/npm_with_retry.sh" install --no-save --package-lock=false \
7882
./.ci/inkbox/sdk/typescript \
7983
@opencode-ai/sdk@1.17.18 @opencode-ai/plugin@1.17.18
80-
- run: npm install -g opencode-ai@latest
84+
- run: bash "$GITHUB_WORKSPACE/tests/ci/npm_with_retry.sh" install -g opencode-ai@latest
8185
- run: INKBOX_SDK_PATH="$PWD/.ci/inkbox/sdk/typescript" bash scripts/smoke-loader.sh

scripts/nato-marker.mjs

Lines changed: 30 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,39 @@
1+
import { createHash } from "node:crypto";
12
import { pathToFileURL } from "node:url";
23

3-
const RADIO_WORDS = [
4-
"alpha",
5-
"bravo",
6-
"charlie",
7-
"delta",
8-
"echo",
9-
"foxtrot",
10-
"golf",
11-
"hotel",
12-
"india",
13-
"juliet",
14-
"kilo",
15-
"lima",
16-
"mike",
17-
"november",
18-
"oscar",
19-
"papa",
20-
"quebec",
21-
"romeo",
22-
"sierra",
23-
"tango",
24-
"uniform",
25-
"victor",
26-
"whiskey",
27-
"xray",
28-
"yankee",
29-
"zulu",
4+
const SPEECH_WORDS = [
5+
"banana",
6+
"elephant",
7+
"pineapple",
8+
"alligator",
9+
"motorcycle",
10+
"umbrella",
11+
"dinosaur",
12+
"potato",
13+
"computer",
14+
"volcano",
15+
"airplane",
16+
"butterfly",
17+
"kangaroo",
18+
"octopus",
19+
"calendar",
20+
"chocolate",
21+
"hospital",
22+
"library",
23+
"sandwich",
24+
"telescope",
3025
];
3126

3227
export function natoMarker(runId, runAttempt) {
33-
let value = BigInt(runId) * 10n + BigInt(runAttempt);
34-
const used = new Set();
28+
const token = `${runId}-${runAttempt}`;
29+
if (!/^\d+-\d+$/.test(token)) throw new Error("run id and attempt must be numeric");
30+
let value = BigInt(`0x${createHash("sha256").update(token).digest("hex")}`);
31+
const available = [...SPEECH_WORDS];
3532
const marker = [];
36-
for (let count = 0; count < 5; count += 1) {
37-
let index = Number(value % BigInt(RADIO_WORDS.length));
38-
value /= BigInt(RADIO_WORDS.length);
39-
while (used.has(index)) index = (index + 1) % RADIO_WORDS.length;
40-
used.add(index);
41-
marker.push(RADIO_WORDS[index]);
33+
for (let count = 0; count < 3; count += 1) {
34+
const index = Number(value % BigInt(available.length));
35+
value /= BigInt(available.length);
36+
marker.push(available.splice(index, 1)[0]);
4237
}
4338
return marker.join(" ");
4439
}

src/gateway/dispatch.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { ContactResolver } from "./contacts.js";
77
import { normalizeAddress } from "./contacts.js";
88
import type { NotifyOnce } from "./dedup.js";
99
import { deliveryFailureKey, deliveryFailureRecovery } from "./delivery-policy.js";
10+
import { isSuccessfulHostedSmsMessage } from "./hosted-call-registry.js";
1011
import { downloadMedia, mediaDir } from "./media.js";
1112
import { SILENT } from "./prompts.js";
1213
import type {
@@ -50,7 +51,11 @@ export interface DispatchDeps {
5051
// may retry); filtered/ignored events return true (ack, no retry).
5152
export async function dispatchEvent(deps: DispatchDeps, event: VerifiedEvent): Promise<boolean> {
5253
if (event.provider !== "inkbox") {
53-
if (deps.onExternal) await deps.onExternal(event);
54+
if (deps.onExternal) {
55+
void deps
56+
.onExternal(event)
57+
.catch((error) => deps.logger.error("external.dispatch_failed", { error: String(error) }));
58+
}
5459
return true;
5560
}
5661
const type = event.eventType ?? inferType(event.body);
@@ -421,6 +426,10 @@ async function handleDeliveryFailure(
421426
const r = resourceOf(event.body, isText ? "text_message" : "message");
422427
if (str(r?.direction)?.toLowerCase() === "inbound") return true;
423428
const messageId = str(r?.id);
429+
if (isText && messageId && isSuccessfulHostedSmsMessage(messageId)) {
430+
deps.logger.info("dispatch.hosted_sms_delivery_failed");
431+
return true;
432+
}
424433
const recipientRows = Array.isArray(r?.recipients) ? r.recipients : [];
425434
const failedRecipient = recipientRows
426435
.map((item) => record(item))

0 commit comments

Comments
 (0)