Skip to content

Commit 69ceebe

Browse files
committed
fix: report probe failure as a response field in its doc and keep skewed unanswered outcomes
The contract doc still said the daemon "warns instead" although the step now travels on the response beside the warning. And an unanswered outcome with a reason this client does not know (version skew) was dropped entirely; it now stays unanswered with the reason omitted, encoded as an explicit client arm of the confirmation union.
1 parent 4abb056 commit 69ceebe

4 files changed

Lines changed: 52 additions & 12 deletions

File tree

‎packages/contracts/src/application-lifecycle-runtime.ts‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,8 @@ export type LaunchConfirmationUnansweredReason = 'alert-unrecognized' | 'url-own
182182
/**
183183
* A launch-confirmation probe step that failed or whose result could not be verified, so whether a
184184
* prompt is up is unknown. Not an outcome of the confirmation itself: the open reports no
185-
* confirmation for it and the daemon warns instead, keeping probe failure separate from what a
186-
* completed probe saw.
185+
* confirmation for it and carries this step on the response beside a warning, keeping probe
186+
* failure separate from what a completed probe saw.
187187
*/
188188
export type LaunchConfirmationProbeFailure = 'alert-read' | 'alert-accept' | 'runner';
189189

@@ -196,6 +196,19 @@ export type LaunchConfirmationOutcome =
196196
launchConfirmationProbeFailure?: undefined;
197197
}>;
198198

199+
/**
200+
* What a client reports about a launch confirmation. The platform always pairs an `unanswered`
201+
* outcome with its reason; a version-skewed daemon may not, so this adds the arm where only the
202+
* outcome survives and the unknown reason is omitted.
203+
*/
204+
export type LaunchConfirmationClientOutcome =
205+
| LaunchConfirmationOutcome
206+
| Readonly<{
207+
launchConfirmation: 'unanswered';
208+
launchConfirmationReason?: undefined;
209+
launchConfirmationProbeFailure?: undefined;
210+
}>;
211+
199212
/** No confirmation reported: either none was seen or the probe could not read one. */
200213
export type LaunchConfirmationAbsent = Readonly<{
201214
launchConfirmation?: undefined;

‎packages/contracts/src/client-app.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import type { AppsFilter } from './app-inventory.ts';
44
import type {
55
LaunchConfirmationAbsent,
6-
LaunchConfirmationOutcome,
6+
LaunchConfirmationClientOutcome,
77
} from './application-lifecycle-runtime.ts';
88
import type { JsonObject } from './json.ts';
99
import type { SessionSurface } from './session-surface.ts';
@@ -101,8 +101,9 @@ export type AppOpenOptions = AgentDeviceRequestOverrides &
101101

102102
export type AppOpenResult = AppOpenResultBase &
103103
// The same discriminated report the platform outcome carries: a seen confirmation pairs its
104-
// reason, and only the absent arm can name a probe step that could not read one.
105-
(LaunchConfirmationOutcome | LaunchConfirmationAbsent);
104+
// reason, and only the absent arm can name a probe step that could not read one. The extra arm
105+
// covers a daemon older than the reason field, which answers `unanswered` without one.
106+
(LaunchConfirmationClientOutcome | LaunchConfirmationAbsent);
106107

107108
type AppOpenResultBase = {
108109
session: string;

‎src/__tests__/agent-device-client-apps.test.ts‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,29 @@ test('apps.open drops an unrecognized launch confirmation outcome instead of gue
184184
assert.equal('launchConfirmationReason' in result, false);
185185
});
186186

187+
test('apps.open keeps an unanswered outcome whose reason predates this client', async () => {
188+
const setup = createTransport(async (req) => {
189+
if (req.command === 'open') {
190+
return {
191+
ok: true,
192+
data: {
193+
session: 'qa',
194+
appBundleId: 'com.example.app',
195+
launchConfirmation: 'unanswered',
196+
launchConfirmationReason: 'a-reason-from-a-newer-daemon',
197+
},
198+
};
199+
}
200+
throw new Error(`Unexpected command: ${req.command}`);
201+
});
202+
const client = createAgentDeviceClient(setup.config, { transport: setup.transport });
203+
204+
const result = await client.apps.open({ app: 'com.example.app', platform: 'ios' });
205+
206+
assert.equal(result.launchConfirmation, 'unanswered');
207+
assert.equal('launchConfirmationReason' in result, false);
208+
});
209+
187210
test('apps.open reports a typed probe-failure step and drops an unrecognized one', async () => {
188211
const responses = [
189212
{ step: 'alert-accept', reported: true },

‎src/client/client-normalizers.ts‎

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { DaemonWireRequest, InternalRequestOptions } from '@agent-device/co
22
import type {
33
LaunchConfirmation,
44
LaunchConfirmationAbsent,
5-
LaunchConfirmationOutcome,
5+
LaunchConfirmationClientOutcome,
66
LaunchConfirmationProbeFailure,
77
LaunchConfirmationUnansweredReason,
88
} from '@agent-device/contracts/application-lifecycle-runtime';
@@ -387,19 +387,22 @@ export function normalizeOpenForegroundComposition(data: Record<string, unknown>
387387
}
388388

389389
/**
390-
* The launch confirmation an open response reports, keyed on the typed outcome. The daemon pairs
391-
* an `unanswered` outcome with its reason, so an outcome or reason it does not recognize is dropped
392-
* rather than guessed, as is a probe step outside the typed set; warnings travel separately and
393-
* still reach the caller. With no outcome, the only report left is the typed probe-failure step.
390+
* The launch confirmation an open response reports, keyed on the typed outcome. An outcome or
391+
* reason outside the typed set is dropped rather than guessed, as is a probe step outside its
392+
* typed set; warnings travel separately and still reach the caller. An `unanswered` whose reason
393+
* the client does not recognize (version skew) stays `unanswered` without a reason: the prompt is
394+
* on the device either way. With no outcome, the only report left is the typed probe-failure step.
394395
*/
395396
export function normalizeLaunchConfirmation(
396397
data: Record<string, unknown>,
397-
): LaunchConfirmationOutcome | LaunchConfirmationAbsent {
398+
): LaunchConfirmationClientOutcome | LaunchConfirmationAbsent {
398399
const outcome = readLaunchConfirmationOutcome(data.launchConfirmation);
399400
if (outcome === 'accepted') return { launchConfirmation: 'accepted' };
400401
if (outcome === 'unanswered') {
401402
const reason = readLaunchConfirmationReason(data.launchConfirmationReason);
402-
return reason ? { launchConfirmation: 'unanswered', launchConfirmationReason: reason } : {};
403+
return reason
404+
? { launchConfirmation: 'unanswered', launchConfirmationReason: reason }
405+
: { launchConfirmation: 'unanswered' };
403406
}
404407
const probeFailure = readLaunchConfirmationProbeFailure(data.launchConfirmationProbeFailure);
405408
return probeFailure ? { launchConfirmationProbeFailure: probeFailure } : {};

0 commit comments

Comments
 (0)