@margay/ccl-core 1.5.1-alpha.2 → 1.5.1-alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +4196 -3676
- package/dist/sdk/background.d.ts +14 -0
- package/dist/sdk/background.js +3 -1
- package/dist/sdk/cancellation.d.ts +15 -0
- package/dist/sdk/cancellation.js +18 -0
- package/dist/sdk/index.d.ts +2 -0
- package/dist/sdk/query.d.ts +1 -0
- package/dist/sdk/query.js +9 -0
- package/dist/sdk/queryHandle.js +28 -8
- package/dist/sdk/queryLifecycleTypes.d.ts +2 -2
- package/dist/sdk/scheduledNotificationTypes.d.ts +53 -0
- package/dist/sdk/scheduledNotificationTypes.js +18 -0
- package/dist/sdk/types.d.ts +4 -1
- package/docs/background-runs.md +46 -2
- package/docs/releases/1.5.1-alpha.3.md +23 -0
- package/docs/releases/1.5.1-alpha.4.md +27 -0
- package/package.json +4 -2
- package/sdk/API.md +67 -1
package/dist/sdk/background.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { BackgroundRun, BackgroundRunAction, BackgroundRunApproval, BackgroundRunEvents, BackgroundRunLookup, BackgroundRunSelector, BackgroundRunSpec, BackgroundRunSource } from './backgroundRunTypes.js';
|
|
2
2
|
import type { OccurrenceCancelRequest, OccurrenceApprovalRequest, OccurrenceApprovalReceipt, OccurrenceLookup, OccurrenceEvents, ScheduledOccurrence } from './scheduledOccurrenceTypes.js';
|
|
3
|
+
import type { ScheduledNotificationSelector, ScheduledNotificationRebind, ScheduledNotificationBindingLookup } from './scheduledNotificationTypes.js';
|
|
3
4
|
export type BackgroundClientOptions = {
|
|
4
5
|
cwd: string;
|
|
5
6
|
cliPath: string;
|
|
@@ -19,6 +20,19 @@ export declare class BackgroundClientError extends Error {
|
|
|
19
20
|
/** Durable runs do not retain JS callbacks or in-process SDK MCP servers. Use
|
|
20
21
|
* persistent stdio/HTTP tools configured through launch instead. */
|
|
21
22
|
export declare function createBackgroundClient(options: BackgroundClientOptions): {
|
|
23
|
+
getScheduledNotificationBinding: (selector: ScheduledNotificationSelector) => Promise<ScheduledNotificationBindingLookup>;
|
|
24
|
+
rebindScheduledNotifications: (request: ScheduledNotificationRebind) => Promise<{
|
|
25
|
+
selector: {
|
|
26
|
+
taskId: string;
|
|
27
|
+
taskCreatedAt: number;
|
|
28
|
+
} | {
|
|
29
|
+
occurrenceId: string;
|
|
30
|
+
};
|
|
31
|
+
sessionId: string;
|
|
32
|
+
revision: number;
|
|
33
|
+
generation: string;
|
|
34
|
+
updatedAt: number;
|
|
35
|
+
}>;
|
|
22
36
|
getOccurrence: (request: {
|
|
23
37
|
occurrenceId: string;
|
|
24
38
|
}) => Promise<OccurrenceLookup>;
|
package/dist/sdk/background.js
CHANGED
|
@@ -18,7 +18,7 @@ export function createBackgroundClient(options) {
|
|
|
18
18
|
// Serialization can throw (for example for a cyclic input); do it before
|
|
19
19
|
// allocating a subprocess that would otherwise wait forever for stdin.
|
|
20
20
|
const input = JSON.stringify(request);
|
|
21
|
-
const retryAdvice = command.includes('occurrence') ? 'Read the original occurrence and approvalDecisions. Retry approval only with the same actionId and payload.' : 'Query the original requestKey; do not submit a new key.';
|
|
21
|
+
const retryAdvice = command.includes('notification') ? 'Read the original notification binding. Retry a binding mutation only with the same actionId and payload.' : command.includes('occurrence') ? 'Read the original occurrence and approvalDecisions. Retry approval only with the same actionId and payload.' : 'Query the original requestKey; do not submit a new key.';
|
|
22
22
|
return new Promise((resolve, reject) => {
|
|
23
23
|
const child = spawn(options.execPath ?? process.execPath, [options.cliPath, 'daemon', command, '--cwd', options.cwd, '--request-stdin'], {
|
|
24
24
|
cwd: options.cwd, env: options.env ?? process.env, stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -73,6 +73,8 @@ export function createBackgroundClient(options) {
|
|
|
73
73
|
});
|
|
74
74
|
}
|
|
75
75
|
return {
|
|
76
|
+
getScheduledNotificationBinding: (selector) => invoke('get-scheduled-notification-binding', { selector }),
|
|
77
|
+
rebindScheduledNotifications: (request) => invoke('rebind-scheduled-notifications', request),
|
|
76
78
|
getOccurrence: (request) => invoke('get-occurrence', request),
|
|
77
79
|
listOccurrences: (request = {}) => invoke('list-occurrences', request),
|
|
78
80
|
readOccurrenceEvents: (request) => invoke('occurrence-events', request),
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { z } from 'zod/v4';
|
|
2
|
+
/** Control-channel provenance, not the identity or intent of a person. */
|
|
3
|
+
export declare const hostCancellationSchema: z.ZodObject<{
|
|
4
|
+
origin: z.ZodLiteral<"host">;
|
|
5
|
+
reason: z.ZodEnum<{
|
|
6
|
+
cancelled: "cancelled";
|
|
7
|
+
timeout: "timeout";
|
|
8
|
+
connection_closed: "connection_closed";
|
|
9
|
+
callback_error: "callback_error";
|
|
10
|
+
permission_denied: "permission_denied";
|
|
11
|
+
}>;
|
|
12
|
+
}, z.core.$strict>;
|
|
13
|
+
export type HostCancellation = z.infer<typeof hostCancellationSchema>;
|
|
14
|
+
export declare function readHostCancellation(value: unknown): HostCancellation | undefined;
|
|
15
|
+
export declare function hostCancellationMessage(cancellation: HostCancellation, execution?: 'not_started' | 'unknown'): string;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { z } from 'zod/v4';
|
|
2
|
+
/** Control-channel provenance, not the identity or intent of a person. */
|
|
3
|
+
export const hostCancellationSchema = z.object({
|
|
4
|
+
origin: z.literal('host'),
|
|
5
|
+
reason: z.enum(['cancelled', 'timeout', 'connection_closed', 'callback_error', 'permission_denied']),
|
|
6
|
+
}).strict();
|
|
7
|
+
export function readHostCancellation(value) {
|
|
8
|
+
const parsed = hostCancellationSchema.safeParse(value);
|
|
9
|
+
return parsed.success ? parsed.data : undefined;
|
|
10
|
+
}
|
|
11
|
+
export function hostCancellationMessage(cancellation, execution = 'unknown') {
|
|
12
|
+
return `Host control channel ended this turn (${JSON.stringify(cancellation)}). ` +
|
|
13
|
+
(cancellation.reason === 'permission_denied'
|
|
14
|
+
? 'The host ended the turn after a permission denial; preserve the recorded decision without inferring who made it. '
|
|
15
|
+
: 'This lifecycle cancellation is not a user permission refusal or a user preference. ') +
|
|
16
|
+
(execution === 'not_started' ? 'This tool call was not executed. ' : 'A missing result does not establish whether an in-flight operation had effects. ') +
|
|
17
|
+
'Stop this turn. On a later authorized continuation, retain completed results and check existing execution evidence before repeating side effects. This cancellation grants no permission and does not revoke unrelated existing authorization.';
|
|
18
|
+
}
|
package/dist/sdk/index.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export { buildCliArgs, buildCliEnv, decideFailClosed, getSessionUsage, prepareSk
|
|
|
9
9
|
export { query, queryHandleWithTransport } from './queryHandle.js';
|
|
10
10
|
export { createSession, Session, type SessionOptions, type SessionControls, type Turn, type TurnResult, type InputMetadata } from './session.js';
|
|
11
11
|
export type { InputProvenance } from './inputProvenance.js';
|
|
12
|
+
export type { HostCancellation } from './cancellation.js';
|
|
12
13
|
export { readQueryLifecycle, type QueryLifecycleRead } from './queryLifecycle.js';
|
|
13
14
|
export type { QueryLifecycleRecord, QueryLifecycleObservation, QueryLifecycleWrite } from './queryLifecycleTypes.js';
|
|
14
15
|
export { RuntimeResourcesSchema, RuntimeResourceSchema, type RuntimeResources, type RuntimeResource } from './runtimeResources.js';
|
|
@@ -20,6 +21,7 @@ export type { GatewayRequestPhase, GatewayRequestProgress } from './requestProgr
|
|
|
20
21
|
export type { AccumulatedUsage, CanUseToolContext, CanUseToolFn, CanUseToolResult, InitializationResult, LifecycleHooks, PendingApproval, PendingDecision, ProviderConfig, Query, QueryOptions, SDKEvent, SDKUserMessage, SessionStore, SessionLifecycleEvent, Transport, Usage, WireMessage, } from './types.js';
|
|
21
22
|
export { getExecutionGuidance } from './executionGuidance.js';
|
|
22
23
|
export { createBackgroundClient, BackgroundClientError, type BackgroundClientOptions } from './background.js';
|
|
24
|
+
export type { ScheduledNotificationSelector, ScheduledNotificationRebind, ScheduledNotificationBinding, ScheduledNotificationBindingLookup, EffectiveScheduledNotificationRoute } from './scheduledNotificationTypes.js';
|
|
23
25
|
export type { BackgroundRun, BackgroundRunSpec, BackgroundRunLookup, BackgroundRunSelector, BackgroundRunEvent, BackgroundRunEvents, BackgroundRunAction, BackgroundRunApproval, BackgroundPermission, BackgroundRunSource } from './backgroundRunTypes.js';
|
|
24
26
|
export { runCommandProcess, type CommandProcessInput, type CommandProcessResult } from './commandProcess.js';
|
|
25
27
|
export { createCommandAdapter, type CommandAdapter, type CommandAdapterSpec, type CommandDefinition, type CommandOperation, type CommandOutcome, type CommandExecution, type JsonValue } from './commandAdapter.js';
|
package/dist/sdk/query.d.ts
CHANGED
package/dist/sdk/query.js
CHANGED
|
@@ -34,6 +34,7 @@ import { existsSync, mkdirSync, mkdtempSync, realpathSync, rmSync, statSync, sym
|
|
|
34
34
|
import { createHash } from 'crypto';
|
|
35
35
|
import { describeError, errorDetailsList, sanitizeErrorEnvelope, sanitizeErrorText, sanitizeRecoveryDiagnostic, sanitizeRecoveryOutcome } from './errorDiagnostics.js';
|
|
36
36
|
import { interactionResponseSchema } from './interactionResponseSchema.js';
|
|
37
|
+
import { hostCancellationSchema } from './cancellation.js';
|
|
37
38
|
import { tmpdir } from 'os';
|
|
38
39
|
import { dirname, join, resolve } from 'path';
|
|
39
40
|
import { fileURLToPath } from 'url';
|
|
@@ -196,6 +197,7 @@ export async function decideFailClosed(callback, toolName, input, context, timeo
|
|
|
196
197
|
const deny = (message, failClosedReason) => ({
|
|
197
198
|
kind: 'decision',
|
|
198
199
|
decision: { behavior: 'deny', message, toolUseID: context.toolUseId,
|
|
200
|
+
cancellation: { origin: 'host', reason: timedOut ? 'timeout' : 'callback_error' },
|
|
199
201
|
...(toolName === 'AskUserQuestion' ? { interaction: { outcome: timedOut ? 'timed_out' : 'interaction_unavailable' } } : {}),
|
|
200
202
|
},
|
|
201
203
|
failClosedReason,
|
|
@@ -239,6 +241,10 @@ export async function decideFailClosed(callback, toolName, input, context, timeo
|
|
|
239
241
|
};
|
|
240
242
|
}
|
|
241
243
|
case 'deny': {
|
|
244
|
+
const cancellation = hostCancellationSchema.safeParse(raw.cancellation);
|
|
245
|
+
if (raw.cancellation !== undefined && !cancellation.success) {
|
|
246
|
+
return deny('Cancellation provenance was invalid; no user choice was inferred.', 'unrecognized_result');
|
|
247
|
+
}
|
|
242
248
|
const interaction = interactionResponseSchema.safeParse(raw.interaction);
|
|
243
249
|
if (raw.interaction !== undefined && !interaction.success) {
|
|
244
250
|
return deny('Interaction response was invalid; no user choice was inferred.', 'unrecognized_result');
|
|
@@ -250,6 +256,8 @@ export async function decideFailClosed(callback, toolName, input, context, timeo
|
|
|
250
256
|
message: sanitizeErrorText(raw.message ?? 'Denied by host'),
|
|
251
257
|
...(toolName === 'AskUserQuestion' && interaction.success ? { interaction: interaction.data } : {}),
|
|
252
258
|
...(raw.interrupt !== undefined && { interrupt: raw.interrupt }),
|
|
259
|
+
...(cancellation.success ? { cancellation: cancellation.data } : raw.interrupt
|
|
260
|
+
? { cancellation: { origin: 'host', reason: 'permission_denied' } } : {}),
|
|
253
261
|
toolUseID: context.toolUseId,
|
|
254
262
|
},
|
|
255
263
|
};
|
|
@@ -699,6 +707,7 @@ export async function* queryWithTransport(prompt, options, transport) {
|
|
|
699
707
|
toolUseId,
|
|
700
708
|
behavior: outcome.decision.behavior,
|
|
701
709
|
...(outcome.decision.behavior === 'deny' && outcome.decision.interaction ? { interaction: outcome.decision.interaction } : {}),
|
|
710
|
+
...(outcome.decision.behavior === 'deny' && outcome.decision.cancellation ? { cancellation: outcome.decision.cancellation } : {}),
|
|
702
711
|
source: 'callback',
|
|
703
712
|
...(outcome.failClosedReason && {
|
|
704
713
|
failClosedReason: outcome.failClosedReason,
|
package/dist/sdk/queryHandle.js
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
*/
|
|
26
26
|
import { queryWithTransport, spawnForOptions } from './query.js';
|
|
27
27
|
import { isAsyncInput } from './queryInput.js';
|
|
28
|
+
import { hostCancellationSchema } from './cancellation.js';
|
|
28
29
|
let requestCounter = 0;
|
|
29
30
|
const nextRequestId = () => `ccl-sdk-req-${++requestCounter}-${Math.random().toString(36).slice(2, 10)}`;
|
|
30
31
|
class QueryHandle {
|
|
@@ -81,14 +82,15 @@ class QueryHandle {
|
|
|
81
82
|
// The init promise is settled internally; hosts that never ask for the
|
|
82
83
|
// initialization result must not see an unhandled rejection.
|
|
83
84
|
this.initPromise.catch(() => { });
|
|
84
|
-
|
|
85
|
+
const retired = () => this.cancelPermission?.({ origin: 'host', reason: 'connection_closed' });
|
|
86
|
+
void transport.exited.then(retired, retired);
|
|
85
87
|
const callback = options.canUseTool;
|
|
86
88
|
void this.pump(prompt, callback ? {
|
|
87
89
|
...options,
|
|
88
90
|
canUseTool: async (...args) => {
|
|
89
91
|
let cancel;
|
|
90
92
|
const cancelled = new Promise(resolve => {
|
|
91
|
-
cancel =
|
|
93
|
+
cancel = cancellation => resolve({ behavior: 'deny', message: 'Permission wait cancelled by host', interrupt: true, cancellation });
|
|
92
94
|
});
|
|
93
95
|
this.cancelPermission = cancel;
|
|
94
96
|
try {
|
|
@@ -220,9 +222,12 @@ class QueryHandle {
|
|
|
220
222
|
if (response.subtype === 'error') {
|
|
221
223
|
entry.reject(new Error(String(response.error ?? 'control request failed')));
|
|
222
224
|
}
|
|
223
|
-
else {
|
|
225
|
+
else if (response.subtype === 'success') {
|
|
224
226
|
entry.resolve(response.response);
|
|
225
227
|
}
|
|
228
|
+
else {
|
|
229
|
+
entry.reject(new Error('ccl-sdk: invalid control acknowledgement'));
|
|
230
|
+
}
|
|
226
231
|
}
|
|
227
232
|
trackInitialize(requestId) {
|
|
228
233
|
this.pending.set(requestId, {
|
|
@@ -278,19 +283,34 @@ class QueryHandle {
|
|
|
278
283
|
return Promise.reject(new Error(this.endReason));
|
|
279
284
|
const requestId = nextRequestId();
|
|
280
285
|
return new Promise((resolve, reject) => {
|
|
281
|
-
|
|
286
|
+
const timer = opts.timeoutMs ? setTimeout(() => {
|
|
287
|
+
const entry = this.pending.get(requestId);
|
|
288
|
+
this.pending.delete(requestId);
|
|
289
|
+
entry?.reject(new Error('ccl-sdk: interrupt acknowledgement timed out; stop outcome is unknown'));
|
|
290
|
+
}, opts.timeoutMs) : undefined;
|
|
291
|
+
this.pending.set(requestId, {
|
|
292
|
+
resolve: value => { if (timer)
|
|
293
|
+
clearTimeout(timer); resolve(value); },
|
|
294
|
+
reject: error => { if (timer)
|
|
295
|
+
clearTimeout(timer); reject(error); },
|
|
296
|
+
});
|
|
282
297
|
this.transport
|
|
283
298
|
.write({ type: 'control_request', request_id: requestId, request })
|
|
284
299
|
.catch(error => {
|
|
285
300
|
if (this.pending.delete(requestId)) {
|
|
301
|
+
if (timer)
|
|
302
|
+
clearTimeout(timer);
|
|
286
303
|
reject(error instanceof Error ? error : new Error(String(error)));
|
|
287
304
|
}
|
|
288
305
|
});
|
|
289
306
|
});
|
|
290
307
|
}
|
|
291
|
-
interrupt() {
|
|
292
|
-
|
|
293
|
-
|
|
308
|
+
interrupt(cancellation = { origin: 'host', reason: 'cancelled' }) {
|
|
309
|
+
const parsed = hostCancellationSchema.safeParse(cancellation);
|
|
310
|
+
if (!parsed.success)
|
|
311
|
+
return Promise.reject(new Error('ccl-sdk: invalid cancellation provenance'));
|
|
312
|
+
this.cancelPermission?.(parsed.data);
|
|
313
|
+
return this.controlRequest({ subtype: 'interrupt', cancellation: parsed.data }, { timeoutMs: 10000 });
|
|
294
314
|
}
|
|
295
315
|
setPermissionMode(mode) {
|
|
296
316
|
return this.controlRequest({ subtype: 'set_permission_mode', mode });
|
|
@@ -372,7 +392,7 @@ class QueryHandle {
|
|
|
372
392
|
return (await this.initializationResult()).account;
|
|
373
393
|
}
|
|
374
394
|
async close() {
|
|
375
|
-
this.cancelPermission?.();
|
|
395
|
+
this.cancelPermission?.({ origin: 'host', reason: 'connection_closed' });
|
|
376
396
|
this.markDead('ccl-sdk: query closed');
|
|
377
397
|
await this.transport.close();
|
|
378
398
|
}
|
|
@@ -28,8 +28,8 @@ export declare const QueryLifecycleSchema: z.ZodObject<{
|
|
|
28
28
|
actor: z.ZodEnum<{
|
|
29
29
|
unknown: "unknown";
|
|
30
30
|
agent: "agent";
|
|
31
|
-
human: "human";
|
|
32
31
|
host: "host";
|
|
32
|
+
human: "human";
|
|
33
33
|
}>;
|
|
34
34
|
source: z.ZodString;
|
|
35
35
|
session_id: z.ZodOptional<z.ZodString>;
|
|
@@ -113,8 +113,8 @@ export declare const QueryLifecycleObservationSchema: z.ZodObject<{
|
|
|
113
113
|
actor: z.ZodEnum<{
|
|
114
114
|
unknown: "unknown";
|
|
115
115
|
agent: "agent";
|
|
116
|
-
human: "human";
|
|
117
116
|
host: "host";
|
|
117
|
+
human: "human";
|
|
118
118
|
}>;
|
|
119
119
|
source: z.ZodString;
|
|
120
120
|
session_id: z.ZodOptional<z.ZodString>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { z } from 'zod/v4';
|
|
2
|
+
export declare const scheduledNotificationSelectorSchema: z.ZodUnion<readonly [z.ZodObject<{
|
|
3
|
+
taskId: z.ZodString;
|
|
4
|
+
taskCreatedAt: z.ZodNumber;
|
|
5
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
6
|
+
occurrenceId: z.ZodString;
|
|
7
|
+
}, z.core.$strict>]>;
|
|
8
|
+
export type ScheduledNotificationSelector = z.infer<typeof scheduledNotificationSelectorSchema>;
|
|
9
|
+
export declare const scheduledNotificationLookupSchema: z.ZodObject<{
|
|
10
|
+
selector: z.ZodUnion<readonly [z.ZodObject<{
|
|
11
|
+
taskId: z.ZodString;
|
|
12
|
+
taskCreatedAt: z.ZodNumber;
|
|
13
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
14
|
+
occurrenceId: z.ZodString;
|
|
15
|
+
}, z.core.$strict>]>;
|
|
16
|
+
}, z.core.$strict>;
|
|
17
|
+
export declare const scheduledNotificationRebindSchema: z.ZodObject<{
|
|
18
|
+
selector: z.ZodUnion<readonly [z.ZodObject<{
|
|
19
|
+
taskId: z.ZodString;
|
|
20
|
+
taskCreatedAt: z.ZodNumber;
|
|
21
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
22
|
+
occurrenceId: z.ZodString;
|
|
23
|
+
}, z.core.$strict>]>;
|
|
24
|
+
targetSessionId: z.ZodString;
|
|
25
|
+
expectedRevision: z.ZodNumber;
|
|
26
|
+
actionId: z.ZodString;
|
|
27
|
+
}, z.core.$strict>;
|
|
28
|
+
export type ScheduledNotificationRebind = z.infer<typeof scheduledNotificationRebindSchema>;
|
|
29
|
+
export declare const scheduledNotificationBindingSchema: z.ZodObject<{
|
|
30
|
+
selector: z.ZodUnion<readonly [z.ZodObject<{
|
|
31
|
+
taskId: z.ZodString;
|
|
32
|
+
taskCreatedAt: z.ZodNumber;
|
|
33
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
34
|
+
occurrenceId: z.ZodString;
|
|
35
|
+
}, z.core.$strict>]>;
|
|
36
|
+
sessionId: z.ZodString;
|
|
37
|
+
revision: z.ZodNumber;
|
|
38
|
+
generation: z.ZodString;
|
|
39
|
+
updatedAt: z.ZodNumber;
|
|
40
|
+
}, z.core.$strict>;
|
|
41
|
+
export type ScheduledNotificationBinding = z.infer<typeof scheduledNotificationBindingSchema>;
|
|
42
|
+
export type EffectiveScheduledNotificationRoute = {
|
|
43
|
+
sessionId: string | null;
|
|
44
|
+
source: 'occurrence' | 'task' | 'delegation' | 'unbound';
|
|
45
|
+
binding?: ScheduledNotificationBinding;
|
|
46
|
+
};
|
|
47
|
+
export type ScheduledNotificationBindingLookup = {
|
|
48
|
+
selector: ScheduledNotificationSelector;
|
|
49
|
+
revision: number;
|
|
50
|
+
binding: ScheduledNotificationBinding | null;
|
|
51
|
+
/** For occurrence selectors, includes inherited task routing. */
|
|
52
|
+
effective?: EffectiveScheduledNotificationRoute;
|
|
53
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { z } from 'zod/v4';
|
|
2
|
+
export const scheduledNotificationSelectorSchema = z.union([
|
|
3
|
+
z.strictObject({ taskId: z.string().min(1), taskCreatedAt: z.number().int().nonnegative() }),
|
|
4
|
+
z.strictObject({ occurrenceId: z.string().regex(/^[a-f0-9]{64}$/) }),
|
|
5
|
+
]);
|
|
6
|
+
export const scheduledNotificationLookupSchema = z.strictObject({ selector: scheduledNotificationSelectorSchema });
|
|
7
|
+
export const scheduledNotificationRebindSchema = scheduledNotificationLookupSchema.extend({
|
|
8
|
+
targetSessionId: z.string().min(1).max(200),
|
|
9
|
+
expectedRevision: z.number().int().nonnegative(),
|
|
10
|
+
actionId: z.string().min(1).max(200),
|
|
11
|
+
});
|
|
12
|
+
export const scheduledNotificationBindingSchema = z.strictObject({
|
|
13
|
+
selector: scheduledNotificationSelectorSchema,
|
|
14
|
+
sessionId: z.string().min(1).max(200),
|
|
15
|
+
revision: z.number().int().positive(),
|
|
16
|
+
generation: z.string().uuid(),
|
|
17
|
+
updatedAt: z.number().int().nonnegative(),
|
|
18
|
+
});
|
package/dist/sdk/types.d.ts
CHANGED
|
@@ -29,6 +29,8 @@ export type CanUseToolResult = {
|
|
|
29
29
|
interaction?: import('./interactionResponseSchema.js').InteractionResponse;
|
|
30
30
|
/** Also interrupt the current turn instead of letting the agent continue. */
|
|
31
31
|
interrupt?: boolean;
|
|
32
|
+
/** Host control-channel provenance; not a human refusal or a new authorization. */
|
|
33
|
+
cancellation?: import('./cancellation.js').HostCancellation;
|
|
32
34
|
} | {
|
|
33
35
|
/**
|
|
34
36
|
* Suspend the session at this tool call (issue P0-2 pending): the tool
|
|
@@ -403,6 +405,7 @@ type SDKEventPayload = {
|
|
|
403
405
|
behavior: 'allow' | 'deny';
|
|
404
406
|
/** Why the SDK decided this way when the host callback did not decide. */
|
|
405
407
|
failClosedReason?: string;
|
|
408
|
+
cancellation?: import('./cancellation.js').HostCancellation;
|
|
406
409
|
interaction?: import('./interactionResponseSchema.js').InteractionResponse;
|
|
407
410
|
/** 'pending_decision' when resolved by resumeWithDecision, not the live callback. */
|
|
408
411
|
source?: 'callback' | 'pending_decision';
|
|
@@ -479,7 +482,7 @@ export type InitializationResult = {
|
|
|
479
482
|
* the stream is identical to the bare queryWithTransport generator.
|
|
480
483
|
*/
|
|
481
484
|
export interface Query extends AsyncGenerator<SDKEvent, void, unknown> {
|
|
482
|
-
interrupt(): Promise<unknown>;
|
|
485
|
+
interrupt(cancellation?: import('./cancellation.js').HostCancellation): Promise<unknown>;
|
|
483
486
|
setPermissionMode(mode: string): Promise<unknown>;
|
|
484
487
|
setModel(model?: string): Promise<unknown>;
|
|
485
488
|
setMaxThinkingTokens(n: number | null): Promise<unknown>;
|
package/docs/background-runs.md
CHANGED
|
@@ -93,6 +93,24 @@ await background.cancelRun({ runId, expectedRevision, actionId: 'cancel-1' })
|
|
|
93
93
|
|
|
94
94
|
`ccl daemon submit|get|events|cancel-run|approve-run --request-stdin --cwd PROJECT` 从 stdin 接收 JSON。submit 可启动缺失的兼容 daemon;读取不会启动进程。旧活 owner 配置不匹配时明确拒绝,不暗中替换。daemon stop 停调度;取消某一 run 使用 cancel-run。
|
|
95
95
|
|
|
96
|
+
### 安装升级与已有定时任务
|
|
97
|
+
|
|
98
|
+
安装新包不会重启正在运行的 daemon。`ccl daemon status --cwd PROJECT`、`scheduledStatus` 和 `CronList` 的 background 结果包含:
|
|
99
|
+
|
|
100
|
+
- `runningCliDigest`:通过活 owner 协议核实的启动时 CLI SHA-256;旧 owner 未保存该字段或没有核实到活 owner 时为 `null`。
|
|
101
|
+
- `installedCliDigest`:执行这次查询的 CLI 文件 SHA-256;读取失败时为 `null`。嵌入 service 的调用者可显式传比较路径。
|
|
102
|
+
- `cliCompatibility`:`match`、`mismatch` 或 `unknown`,另有 `cliCompatibilityReason` 解释不同或未知的原因。
|
|
103
|
+
|
|
104
|
+
`owner.ready` 仍表示已获得调度租约,不等于版本一致或任务已完成。摘要一致只表示 CLI 字节一致,不证明启动配置一致。旧 owner 的 `configId` 不能反推其 CLI 摘要;升级覆盖同一路径后也不能从新文件推断旧进程版本。`CronCreate` 的后台 handoff 结果也带相同的摘要字段。
|
|
105
|
+
|
|
106
|
+
产品启动或巡检可只读查询并提示用户。需要切换时,保留任务 ID、原启动配置及 occurrence 状态,由用户决定时机;明确执行 `ccl daemon stop --cwd PROJECT`,确认旧 owner 已退出、租约已释放,再从原宿主以相同配置对原 taskId 重试 `CronCreate operation:handoff`。采用 CLI start 时也要保留原宿主对应的配置;无条件裸 start 可能使用不同模型/设置,导致下一次 handoff 仍拒绝。不要删除重建排程,也不要循环重试不同配置或自动杀进程。stop 超时先查原 owner 状态,不能把超时当成已停止。
|
|
107
|
+
|
|
108
|
+
已接受的 occurrence 独立于排程定义:删除定义只影响未来触发,旧 occurrence 仍可按原 ID 查询、按明确决定取消;取消不会撤销已经发生的外部效果。新 owner 保留 waiting_permission,running/resuming 的不确定执行按 interrupted 处理,不自动重放。错过时点仍按原 missed/coalesce 规则处理,不意味着所有错过任务都自动补跑。
|
|
109
|
+
|
|
110
|
+
恢复审批必须同时存在原 session 的 confirmed pending 与精确匹配的 suspended 事件。`pendingPermission` 缺失时,新 owner 尝试恢复;失败会保留 `approvalRecoveryError`,不能编造 request/tool ID 批准。alpha.4 的原生 pending 没有新版 confirmed 挂起协议,因此不能承诺它能直接恢复,也不能补写 confirmed 绕过检查。由获授权的项目宿主先查询原 occurrence、核实已有外部效果,再按用户决定保留或使用 `cancel-occurrence`(snapshot.revision + 稳定 actionId)取消;不会自动取消或重新执行整项工作。
|
|
111
|
+
|
|
112
|
+
未显式绑定时,通知仍按原 `delegation.sourceSessionId` 投递。新会话可以使用下面的独立换绑接口;没有 delegation 或已删定义的旧 occurrence 也能绑定。`reauthorize` 只更新未来触发的权限委托,不改变既有 occurrence 或显式通知绑定。
|
|
113
|
+
|
|
96
114
|
| 目的 | SDK | CLI 子命令 | `BackgroundRun` 模型工具 action |
|
|
97
115
|
| --- | --- | --- | --- |
|
|
98
116
|
| 持久接受 | `submitRun` | `submit` | `submit` |
|
|
@@ -217,11 +235,37 @@ CLI 使用 `ccl daemon <子命令> --request-stdin --cwd PROJECT`,stdin 中为
|
|
|
217
235
|
|
|
218
236
|
只读查询不启动 owner,也不消费通知;决定和取消需要兼容的运行 owner。旧 `daemon approve <id> allow` 无精确身份,返回迁移错误。模型工具用 `BackgroundRun` 的 `occurrence_id` 配合 `get/events/approve/cancel`;批准字段为 `expected_revision/request_id/tool_use_id/action_id/decision`,事件使用 `after_sequence`,不用 run 的 opaque `cursor`。模型收到宿主通知不等于用户授权,allow 仍遵循前台权限确认或明确适用策略。
|
|
219
237
|
|
|
238
|
+
### 新会话接收原任务通知
|
|
239
|
+
|
|
240
|
+
换绑只改变通知收件人,不启动 owner,不删除重建任务,也不改变计划、执行状态、worker session、权限或待批身份。CLI/SDK 是受信本机宿主接口,可指定目标 session;模型工具请求确认,并由宿主填入当前 session,模型不能指定其他会话。
|
|
241
|
+
|
|
242
|
+
- 整项排程:在新会话用 `CronList` 读取原任务的 `notificationBinding.revision`,调用 `CronCreate`,输入 `operation: "rebind-notifications"`、原 `taskId`、`bindingRevision` 和稳定 `actionId`。首次 revision 为 0。此入口要求定义仍存在;一次性任务触发后定义可能已移除。
|
|
243
|
+
- 单次发生:使用 `BackgroundRun action:"notification-binding", occurrence_id:原ID` 读取该 occurrence 的绑定 revision,再用 `action:"rebind-notifications"`、相同 `occurrence_id`、`binding_revision`、稳定 `action_id` 换绑。它适用于定义已删或没有 delegation 的旧记录。
|
|
244
|
+
- SDK/CLI 整项选择器为 `{taskId, taskCreatedAt}`;它匹配已有记录和未来触发,防止同 ID 重建任务串收件人。定义已删时可从原 occurrence 的 task 快照取得身份。单次选择器为 `{occurrenceId}`。两种选择器不能混用。
|
|
245
|
+
|
|
246
|
+
```js
|
|
247
|
+
const selector = { occurrenceId: originalOccurrenceId }
|
|
248
|
+
const binding = await background.getScheduledNotificationBinding(selector)
|
|
249
|
+
const request = {
|
|
250
|
+
selector, targetSessionId: newSessionId,
|
|
251
|
+
expectedRevision: binding.revision, actionId: stableActionId,
|
|
252
|
+
}
|
|
253
|
+
const receipt = await background.rebindScheduledNotifications(request)
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
对应 CLI 为 `ccl daemon get-scheduled-notification-binding --request-stdin --cwd PROJECT`,stdin 内容 `{ "selector": { "occurrenceId": "原64位ID" } }`;写接口为 `rebind-scheduled-notifications`,另加 `targetSessionId`、`expectedRevision`、`actionId`。读取不创建绑定。确认回执不确定时重试原请求;同一 actionId 同一内容返回原回执,不再次换绑。复用 actionId 改内容报 `action_conflict`;新动作遇到 `stale_binding` 时先读现状,再由宿主决定是否发起新的修改。
|
|
257
|
+
|
|
258
|
+
有效路由优先级是单次 occurrence 显式绑定、task 身份绑定、原 delegation。换绑整项任务不会覆盖单次显式选择。lookup 的 revision 只属于所选绑定,不能把 `effective.binding.revision` 当成另一选择器的 revision。`CronList.background` 和 `daemon status` 显示 task 的绑定 revision、收件 session,以及 occurrence 的有效 route;路由文件损坏时报告 `notificationRoutingError`,不会回退投递给原收件人,owner readiness 和 CLI 摘要仍可查询。修复路由存储前先保留原文件并核实目标,不用删文件来隐式恢复旧路由。
|
|
259
|
+
|
|
260
|
+
显式换绑后只补发最新可通知状态,不回放过期审批历史。稳定通知 UUID 包含绑定代次和当前待批身份/终态;coalesce 或 delivery 更新不重复同一待办,新的 pending 或终态会通知。A→B→A 使用新代次,旧 A 回执不会吞掉新通知。只有实际持久 transcript 消息才算接收回执,入队、get/events 或缓存本身都不算。
|
|
261
|
+
|
|
262
|
+
换绑对尚未被消费者 prepare 接受的通知生效:旧队列项在消费前复核路由和当前状态,不匹配则丢弃重取。已经接受或持久保存的旧通知无法撤回,因此不承诺两个会话在换绑瞬间零重叠。收到通知不授予审批权,批准仍须查询原 occurrence 并使用精确 pending 身份。旧格式无法恢复 pending 时,通知报告恢复错误,不生成虚构批准。
|
|
263
|
+
|
|
220
264
|
### 前端体验与故障恢复
|
|
221
265
|
|
|
222
|
-
- TUI 与持续运行的 SDK/headless
|
|
266
|
+
- TUI 与持续运行的 SDK/headless 主循环:有效绑定会话收到 `scheduled-task` 宿主事件,主助手解释具体待批工具、当前结果或失败。此事件不冒充新的用户指令。SDK 的自主通知结果不绑定到上一条用户输入的 turn UUID。
|
|
223
267
|
- 已退出的一次性 headless:不承诺向已关闭的输出流推送。消费者可通过上述 SDK/CLI 查询和处理结构化 pending;恢复原会话时补收未投递消息。Pepper 等 assistant 负责自己的外部消息渠道及送达回执。
|
|
224
|
-
- 接收者离线、入队失败或入队后尚未写入 transcript:保留通知游标,补投稳定 UUID
|
|
268
|
+
- 接收者离线、入队失败或入队后尚未写入 transcript:保留通知游标,补投稳定 UUID。只有接收会话持久保存的通知消息才算宿主接收回执;get/events 和队列操作记录都不算。已保存不代表模型已解释或 IM 已送达。
|
|
225
269
|
- 同一待批调用跨周期只提示一次,查询及实际消费时显示最新合并次数。投递前、队列消费前重查状态,已经批准、拒绝、取消或结束的旧审批不再提问;历史事件仍可读。状态读取失败不会冒充“已解决”。
|
|
226
270
|
- 等待批准时 owner 重启:保留经确认尚未执行的原调用,可在明确批准后恢复原 session。恢复严格核对工具与完整输入,不因同名工具就套用旧批准。
|
|
227
271
|
- 决定和恢复 intent 已保存后崩溃:重启标记 `interrupted`,不自动再执行。先核查原收据、工具结果及适用的 Operation 恢复契约;宿主不保证任意外部系统 exactly-once。
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# 1.5.1-alpha.3
|
|
2
|
+
|
|
3
|
+
本预发布版在 [1.5.1-alpha.2](1.5.1-alpha.2.md) 基础上增加宿主取消来源契约(#58)和视觉输出预算配置及有界恢复(#59)。使用明确版本或 `alpha` 标签安装;npm 发布状态以 registry 为准,`latest` 保持不变。
|
|
4
|
+
|
|
5
|
+
## 宿主停止与权限拒绝
|
|
6
|
+
|
|
7
|
+
SDK `handle.interrupt()`、权限回调超时/异常和会话退役会保留宿主取消来源,不再被误写成用户拒绝或用户偏好。普通工具 Deny 继续保留拒绝依据;取消不撤销其他授权,也不授予新权限。
|
|
8
|
+
|
|
9
|
+
`interrupt()` 可接收 `HostCancellation`,权限回调 deny 可附带 `cancellation`;来源也会出现在 `permission_resolved` 和原生 transcript。旧客户端只返回 `deny('Conversation stopped')` 时仍按拒绝处理,需迁移到 interrupt 或显式 cancellation。AskUserQuestion 的非终止 unavailable/timeout 交互保持原行为,不制造用户答案。
|
|
10
|
+
|
|
11
|
+
取消与结果返回竞态时保留已完成事实,迟到的 allow 不启动已取消的工具。成功停止 ACK 只确认控制请求被接受;拒绝、畸形 ACK、断连或10秒无 ACK 会明确失败,停止结果可能未知。取消不回滚已发生的外部效果,强杀或close也不保证最后消息持久化;恢复前仍需核对事实,不能盲目重放。
|
|
12
|
+
|
|
13
|
+
## 视觉输出预算
|
|
14
|
+
|
|
15
|
+
CLI 环境变量 `CCL_VISUAL_MAX_OUTPUT_TOKENS` 默认8192;可选 `CCL_VISUAL_RECOVERY_MAX_OUTPUT_TOKENS` 显式授权一次更高预算恢复,默认不启用。SDK通过已有 `options.env` 配置。值必须是正安全整数,恢复预算必须大于初始预算;非法配置在读图或模型请求前失败。Agent模型不能通过工具参数改预算或覆盖视觉模型。
|
|
16
|
+
|
|
17
|
+
现有模型输出上限和共享上下文限制仍会约束请求。只有完整终态的输出限额失败、无拒绝/过滤、有效预算确实增加且恢复额度未用时,才会用原模型、协议、提示和已授权图片字节重试。预算被模型上限截平时不发无效重试。空响应与输出限额恢复共用一次内容重试,另允许此前一次能力上下文刷新;至多3次推理共用900秒期限。
|
|
18
|
+
|
|
19
|
+
成功和失败均记录尝试历史、request ID、请求/有效预算、限制来源、重试关联、耗时及累计已知usage。缺少input/output用量时 `usageComplete=false`,不把未知成本当作零。最终未完成不会报视觉通过。成功结果提示也不再无条件暗示此前发生过失败和恢复。
|
|
20
|
+
|
|
21
|
+
提高预算可能增加成本,不保证提升视觉准确性。原三图同提示的真实模型对照中,8192和16384都单次完成,未复现旧截断;本次真实运行未触发自动恢复,恢复边界由CLI/SDK接线及单元测试验证。模型仍有精确颜色判断误差,完整输出不能替代业务验收。
|
|
22
|
+
|
|
23
|
+
使用方式及兼容边界见 [SDK API](../../sdk/API.md)。CLI与SDK保持同版本;已运行进程不会热更新,正常结束后新会话使用新版。未改默认模型、权限策略或业务数据,无需数据迁移。仍不承诺跨外部系统exactly-once、模型始终正确理解图像或任意终止都能回收已发生的副作用。
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# 1.5.1-alpha.4
|
|
2
|
+
|
|
3
|
+
本预发布版在 [1.5.1-alpha.3](1.5.1-alpha.3.md) 基础上交付 #60 的 daemon 升级状态和独立通知换绑。安装指定版本或使用 `alpha` 标签;`latest` 保持不变。CLI 与 SDK 应使用同版本。
|
|
4
|
+
|
|
5
|
+
## 升级状态与旧记录
|
|
6
|
+
|
|
7
|
+
`daemon status`、`CronList.background` 和 `CronCreate` 的后台返回包含运行/安装 CLI 摘要及 `cliCompatibility: match | mismatch | unknown`。旧 owner 没有启动摘要时明确 unknown;不会把升级后同路径的新文件当成旧进程版本。`owner.ready` 与版本一致性分开。
|
|
8
|
+
|
|
9
|
+
安装不会重启现有 daemon。需要切换时保留原任务身份和启动配置,明确 stop 并确认旧 owner 退出,再从原宿主对原 taskId handoff;不要删除重建排程。stop 导致活动 SDK aborted 时保存 interrupted,重启不自动重放;等待审批记录保留,错过周期按既有 missed/coalesce 契约处理。
|
|
10
|
+
|
|
11
|
+
恢复审批仍要求原 session 的 confirmed pending 和精确请求身份。旧 alpha.4 原生 pending 没有该确认协议时,保留恢复错误,不伪造批准或自动重跑;可查询原 occurrence,并在明确授权下取消。删除定义不删除已接受记录,取消不撤销已发生的外部效果。
|
|
12
|
+
|
|
13
|
+
## 新会话通知换绑
|
|
14
|
+
|
|
15
|
+
SDK `getScheduledNotificationBinding` / `rebindScheduledNotifications` 与同名 kebab-case CLI 子命令支持原 `{taskId, taskCreatedAt}` 或 `{occurrenceId}` 选择器。换绑含 `targetSessionId`、`expectedRevision`(首次 0)、稳定 `actionId`;同动作重试返回原回执,冲突和损坏明确报错。
|
|
16
|
+
|
|
17
|
+
模型工具使用 `CronCreate operation:rebind-notifications` 或 `BackgroundRun action:notification-binding/rebind-notifications`;经过权限确认,收件人固定为宿主当前会话。换绑不启动 owner,不改任务、计划、执行、权限或待批身份。单 occurrence 显式绑定优先于 task,最后才使用原 delegation。已删定义的 occurrence 仍可换绑。
|
|
18
|
+
|
|
19
|
+
REPL 和持续 headless 会话使用共同通知实现。新绑定只投当前待办/结果,以持久 transcript 为回执;代次隔离支持 A→B→A,合并周期不重复同一待办。消费前复核会丢弃旧路由队列项,已经接受的旧通知无法撤回,不承诺换绑瞬间零重叠。通知不是审批,仍需查询原 occurrence 后针对精确 pending 决定。
|
|
20
|
+
|
|
21
|
+
## 验证与兼容边界
|
|
22
|
+
|
|
23
|
+
隔离测试覆盖真实旧 alpha.4 产物切换、现代 pending 原会话恢复、运行任务中止与 missed/coalesce,以及最终候选 CLI/SDK、真实 detached owner、两个持续会话、工具拒绝/允许换绑、真实 transcript 和原精确批准一次执行。确定性 provider 证明接线与生命周期,不证明真实模型自主规划质量;REPL 验证为实际 hook/队列消费,不是整屏视觉验收。
|
|
24
|
+
|
|
25
|
+
独立路由是新增可选存储,未绑定任务沿用原路由。旧运行版本不会理解新绑定,启用此功能后不应混用旧前台消费者或直接降级并期待新路由仍生效。包回退不能撤销已经接受的通知或业务效果,回退前需核实接收方与待执行记录。
|
|
26
|
+
|
|
27
|
+
完整升级顺序、CLI/SDK/工具字段、优先级及失败处理见 [后台任务契约](../background-runs.md) 与 [SDK API](../../sdk/API.md)。本版包含 alpha.3 的 #58/#59 修复,不宣称解决视觉模型准确性问题。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@margay/ccl-core",
|
|
3
|
-
"version": "1.5.1-alpha.
|
|
3
|
+
"version": "1.5.1-alpha.4",
|
|
4
4
|
"description": "Core runtime extracted from Claude Code (ccl) for Margay integration",
|
|
5
5
|
"license": "SEE LICENSE IN README.md",
|
|
6
6
|
"type": "module",
|
|
@@ -54,7 +54,9 @@
|
|
|
54
54
|
"docs/releases/1.5.0-alpha.6.md",
|
|
55
55
|
"docs/releases/1.5.1-alpha.md",
|
|
56
56
|
"docs/releases/1.5.1-alpha.1.md",
|
|
57
|
-
"docs/releases/1.5.1-alpha.2.md"
|
|
57
|
+
"docs/releases/1.5.1-alpha.2.md",
|
|
58
|
+
"docs/releases/1.5.1-alpha.3.md",
|
|
59
|
+
"docs/releases/1.5.1-alpha.4.md"
|
|
58
60
|
],
|
|
59
61
|
"engines": {
|
|
60
62
|
"node": ">=22.19.0"
|
package/sdk/API.md
CHANGED
|
@@ -213,7 +213,7 @@ type CanUseToolFn = (
|
|
|
213
213
|
|
|
214
214
|
type CanUseToolResult =
|
|
215
215
|
| { behavior: 'allow'; updatedInput?: Record<string, unknown> } // 放行;updatedInput=改写输入(modify)
|
|
216
|
-
| { behavior: 'deny'; message?: string; interrupt?: boolean
|
|
216
|
+
| { behavior: 'deny'; message?: string; interrupt?: boolean; cancellation?: HostCancellation }
|
|
217
217
|
| { behavior: 'pending' } // 挂起走异步审批(§5)
|
|
218
218
|
```
|
|
219
219
|
|
|
@@ -222,6 +222,12 @@ type CanUseToolResult =
|
|
|
222
222
|
- `allow` 未提供 `updatedInput`(或提供空对象)时按原始 input 执行。
|
|
223
223
|
- 未配置 `canUseTool` 时 SDK 不启用权限转发(工具按 CLI 自身权限规则走);若此时 CLI 仍发来请求,按 deny 兜底。
|
|
224
224
|
|
|
225
|
+
宿主停止与权限拒绝是不同事件。停止生成可直接 `await handle.interrupt()`;它取消未完成权限等待,默认来源为 `{origin:'host',reason:'cancelled'}`。也可传完整的 `HostCancellation`,reason 支持 `cancelled`、`timeout`、`connection_closed`、`callback_error`、`permission_denied`。`origin` 表示控制通道来源,不推断是谁点击了停止。原始 stream-json 请求为 `{type:'control_request',request_id,request:{subtype:'interrupt',cancellation}}`,省略 cancellation 同样表示宿主取消。
|
|
226
|
+
|
|
227
|
+
需要在权限回调结束等待时,可返回 `{behavior:'deny',message:'Conversation stopped',cancellation:{origin:'host',reason:'cancelled'}}`。这结束当前轮,工具不执行;`permission_resolved.cancellation` 与原生 transcript 的 `toolUseResult.cancellation` 保留来源。回调超时/异常自动携带 timeout/callback_error。AskUserQuestion 的 timed_out/interaction_unavailable 继续按已有非终止交互失败处理,不制造用户答案。未带 cancellation 的普通 deny 仍是拒绝;deny+interrupt 记录 permission_denied 并保留原拒绝依据,不推断人工决定。旧客户端的 `deny('Conversation stopped')` 仍是拒绝,宿主应迁移到 interrupt 或显式 cancellation,CCL 不通过文字猜测来源。
|
|
228
|
+
|
|
229
|
+
interrupt 的 Promise 只在匹配成功 ACK 后完成;拒绝、畸形 ACK、断连会 reject,10 秒没有 ACK 也会 reject,表示停止结果未知。还须观察当前轮的 result,不能把 ACK 当作工具已回滚。迟到的 allow 不会启动已取消的工具。已完成结果保留;在途操作缺少结果不能证明没有副作用,恢复时先核对事实,不能盲目重放。取消本身不撤销其他已存在授权,也不授予新权限。`end_session` 支持优雅退役并记录 connection_closed;直接强杀进程或 `close()` 是资源清理,不保证最后一条消息已持久化,需按未知结果恢复。
|
|
230
|
+
|
|
225
231
|
---
|
|
226
232
|
|
|
227
233
|
## 5. pending 挂起 / 跨进程恢复(审批闭环)
|
|
@@ -673,3 +679,63 @@ operation-replay guarantee, and does not introduce a new permission gate.
|
|
|
673
679
|
Check the actual tool schema for `mode`; an older installation with the same
|
|
674
680
|
candidate version string may not have it. An existence precheck followed by normal
|
|
675
681
|
Write is not an equivalent no-replacement contract.
|
|
682
|
+
|
|
683
|
+
## Visual specialist output budget (1.5.1-alpha.3)
|
|
684
|
+
|
|
685
|
+
The synchronous `Agent` specialist `visual-ui-analysis` accepts its existing
|
|
686
|
+
`prompt` and `image_paths`. The host controls its output allowance; the model
|
|
687
|
+
cannot set it through Agent arguments or override the selected vision model.
|
|
688
|
+
CLI processes read `CCL_VISUAL_MAX_OUTPUT_TOKENS` (default `8192`) and optional
|
|
689
|
+
`CCL_VISUAL_RECOVERY_MAX_OUTPUT_TOKENS` (disabled by default). SDK hosts use the
|
|
690
|
+
same variables through `options.env`:
|
|
691
|
+
|
|
692
|
+
```ts
|
|
693
|
+
const handle = query({
|
|
694
|
+
prompt: 'Inspect the supplied rendered slides with the visual specialist.',
|
|
695
|
+
options: {
|
|
696
|
+
env: {
|
|
697
|
+
CCL_VISUAL_MAX_OUTPUT_TOKENS: '16384',
|
|
698
|
+
CCL_VISUAL_RECOVERY_MAX_OUTPUT_TOKENS: '32768',
|
|
699
|
+
},
|
|
700
|
+
// Supply your normal cwd, credentials and permission callback as appropriate.
|
|
701
|
+
},
|
|
702
|
+
})
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
Values must be decimal positive safe integers; the recovery value must exceed
|
|
706
|
+
the initial value. Invalid configuration fails before image reads or model I/O.
|
|
707
|
+
The existing provider/configured output limits and shared context budget still
|
|
708
|
+
cap each request. Unknown limits remain unknown; a larger allowance is not a
|
|
709
|
+
claim that the model supports it. Reasoning and visible text share the allowance.
|
|
710
|
+
|
|
711
|
+
A terminal output-limit failure can use the higher allowance once, only when
|
|
712
|
+
its effective budget increases. If a model caps both requests at 8192, no recovery
|
|
713
|
+
request is sent. Recovery reuses the selected model, protocol, prompt and already
|
|
714
|
+
authorized image bytes. It shares one content retry with normal-terminal empty
|
|
715
|
+
response recovery. A capability-context refresh is separately allowed once before
|
|
716
|
+
content recovery, so at most three inference attempts share one 900-second
|
|
717
|
+
operation deadline. Cancellation, refusal, filtering, incomplete streams and an
|
|
718
|
+
exhausted recovery do not trigger a further content retry.
|
|
719
|
+
|
|
720
|
+
The returned visual facts retain every attempt's request ID, requested and
|
|
721
|
+
effective budget, safe `requestBudget` limit snapshot, duration, and one-based
|
|
722
|
+
`retryOfAttempt` / `retryOfRequestId` links. Both completed and failed inspections
|
|
723
|
+
include `visualDiagnostics.usage`, `usageComplete`, and total `durationMs` (from
|
|
724
|
+
post-image-load resolution to terminal outcome). `usageComplete` concerns reported
|
|
725
|
+
input/output counts; optional cache breakdowns may be absent. Missing usage is
|
|
726
|
+
not proof of zero cost: totals sum known values, and `usageComplete: false` marks
|
|
727
|
+
the incomplete accounting. Successful SDK tool usage retains the same numeric
|
|
728
|
+
totals for compatibility. A final failure remains an incomplete inspection,
|
|
729
|
+
preserves earlier failures, and asks the Owner to stop same-condition retries.
|
|
730
|
+
|
|
731
|
+
Increasing output allowance can increase cost and does not establish visual
|
|
732
|
+
accuracy. Verify page observations separately; a complete response alone is not
|
|
733
|
+
business acceptance. These variables require a build containing this feature;
|
|
734
|
+
the previously published alpha.2 does not support them.
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
## Scheduled notification bindings
|
|
738
|
+
|
|
739
|
+
`createBackgroundClient()` 提供 `getScheduledNotificationBinding(selector)` 与 `rebindScheduledNotifications(request)`,用于新会话接收原定时任务通知。选择器为 `{ taskId, taskCreatedAt }` 或 `{ occurrenceId }`;请求包含 `selector`、`targetSessionId`、`expectedRevision`(初始 0)和稳定 `actionId`。返回绑定含 `sessionId/revision/generation/updatedAt`。读取 occurrence 绑定还返回 `effective` 路由;显式 occurrence 优先于 task,task 优先于原 delegation。
|
|
740
|
+
|
|
741
|
+
换绑不启动 owner、不改变原任务或审批状态。超时重试原 actionId 和原请求;同 ID 改内容拒绝,revision 冲突须读取当前状态后决定新动作。模型工具固定绑定到自己的宿主 session,并请求权限确认。已被消费者接受的旧通知不会撤回;新代次只通知当前待办或结果,不重放过期历史。完整 CLI、工具字段、错误和恢复说明见 [新会话接收原任务通知](../docs/background-runs.md#新会话接收原任务通知)。
|