@margay/ccl-core 1.4.5-rc.9 → 1.5.0-alpha.0

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.
@@ -0,0 +1,53 @@
1
+ import type { Query, QueryOptions, SDKEvent, SDKUserMessage, SessionLifecycleEvent, Transport } from './types.js';
2
+ export type TurnResult = Extract<SDKEvent, {
3
+ type: 'result';
4
+ }>;
5
+ export type SessionControls = Omit<Query, keyof AsyncGenerator<SDKEvent, void, unknown>>;
6
+ export type SessionOptions = QueryOptions & {
7
+ /** Maximum accepted but not yet completed turns, including the active turn. */
8
+ maxPendingTurns?: number;
9
+ /** Events are delivered as they arrive; Session does not retain an event history. */
10
+ onEvent?: (event: SDKEvent) => void;
11
+ /** Deterministic queue/dispatch/permission/terminal observations. No model call.
12
+ * Keep observers fast; thrown errors are isolated from execution. */
13
+ onLifecycle?: (event: SessionLifecycleEvent) => void;
14
+ };
15
+ export interface Turn {
16
+ readonly id: string;
17
+ readonly result: Promise<TurnResult>;
18
+ cancel(): Promise<void>;
19
+ }
20
+ /** One CLI process, sequential turns, and a bounded host-side input queue. */
21
+ export declare class Session {
22
+ readonly pid: number | undefined;
23
+ /** Existing SDK controls, including initialization, model and task controls. */
24
+ readonly controls: SessionControls;
25
+ private readonly handle;
26
+ private readonly limit;
27
+ private readonly options;
28
+ private queue;
29
+ private active?;
30
+ private wake?;
31
+ private accepting;
32
+ private closed;
33
+ private drainWaiters;
34
+ private readonly pumping;
35
+ private sessionId?;
36
+ constructor(options?: SessionOptions, transport?: Transport);
37
+ prompt(content: SDKUserMessage['message']['content']): Turn;
38
+ followUp(content: SDKUserMessage['message']['content']): Turn;
39
+ /** Interrupt the active turn; correction runs before queued follow-ups. */
40
+ steer(content: SDKUserMessage['message']['content']): Turn;
41
+ private enqueue;
42
+ private inputs;
43
+ private consume;
44
+ private observe;
45
+ private fail;
46
+ private notifyDrained;
47
+ interrupt(): Promise<void>;
48
+ private cancel;
49
+ /** Stop accepting inputs and drain accepted turns, keeping controls alive. */
50
+ endInput(): Promise<void>;
51
+ close(): Promise<void>;
52
+ }
53
+ export declare function createSession(options?: SessionOptions): Session;
@@ -0,0 +1,222 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { queryHandleWithTransport } from './queryHandle.js';
3
+ import { spawnForOptions } from './query.js';
4
+ /** One CLI process, sequential turns, and a bounded host-side input queue. */
5
+ export class Session {
6
+ pid;
7
+ /** Existing SDK controls, including initialization, model and task controls. */
8
+ controls;
9
+ handle;
10
+ limit;
11
+ options;
12
+ queue = [];
13
+ active;
14
+ wake;
15
+ accepting = true;
16
+ closed = false;
17
+ drainWaiters = [];
18
+ pumping;
19
+ sessionId;
20
+ constructor(options = {}, transport) {
21
+ this.limit = options.maxPendingTurns ?? 32;
22
+ if (!Number.isSafeInteger(this.limit) || this.limit < 1) {
23
+ throw new RangeError('ccl-sdk: maxPendingTurns must be a positive integer');
24
+ }
25
+ if (options.pendingDecision)
26
+ throw new Error('ccl-sdk: resume a pending decision before creating a Session');
27
+ this.options = options;
28
+ this.sessionId = options.sessionId ?? options.resume;
29
+ const owned = transport ?? spawnForOptions(options);
30
+ this.pid = owned.pid;
31
+ this.handle = queryHandleWithTransport(this.inputs(), { ...options, hooks: {
32
+ ...options.hooks,
33
+ onUserPromptSubmit: async (event) => {
34
+ if (this.active && !this.active.started) {
35
+ this.active.started = true;
36
+ this.observe({ type: 'turn_started', turnId: this.active.turn.id });
37
+ }
38
+ await options.hooks?.onUserPromptSubmit?.(event);
39
+ },
40
+ onSessionStart: async (event) => {
41
+ this.sessionId = event.sessionId;
42
+ await options.hooks?.onSessionStart?.(event);
43
+ },
44
+ } }, owned);
45
+ this.controls = this.handle;
46
+ this.pumping = this.consume();
47
+ }
48
+ prompt(content) {
49
+ return this.enqueue(content, false);
50
+ }
51
+ followUp(content) {
52
+ return this.enqueue(content, false);
53
+ }
54
+ /** Interrupt the active turn; correction runs before queued follow-ups. */
55
+ steer(content) {
56
+ const turn = this.enqueue(content, true);
57
+ if (this.active)
58
+ void this.interrupt().catch(error => this.fail(error));
59
+ return turn;
60
+ }
61
+ enqueue(content, priority) {
62
+ if (!this.accepting || this.closed)
63
+ throw new Error('ccl-sdk: session input is closed');
64
+ if (this.queue.length + (this.active ? 1 : 0) >= this.limit) {
65
+ throw new Error(`ccl-sdk: session queue is full (${this.limit} pending turns)`);
66
+ }
67
+ const id = randomUUID();
68
+ let resolve;
69
+ let reject;
70
+ const result = new Promise((yes, no) => { resolve = yes; reject = no; });
71
+ // It is valid to enqueue follow-ups and observe their results later.
72
+ void result.catch(() => { });
73
+ const turn = { id, result, cancel: () => this.cancel(id) };
74
+ const entry = { turn, resolve, reject, input: {
75
+ type: 'user', uuid: id, message: { role: 'user', content },
76
+ } };
77
+ if (priority)
78
+ this.queue.unshift(entry);
79
+ else
80
+ this.queue.push(entry);
81
+ this.observe({ type: 'turn_queued', turnId: id });
82
+ this.wake?.();
83
+ return turn;
84
+ }
85
+ async *inputs() {
86
+ while (!this.closed) {
87
+ if (this.active) {
88
+ await new Promise(resolve => { this.wake = resolve; });
89
+ this.wake = undefined;
90
+ continue;
91
+ }
92
+ const entry = this.queue.shift();
93
+ if (entry) {
94
+ this.active = entry;
95
+ yield entry.input;
96
+ }
97
+ else {
98
+ await new Promise(resolve => { this.wake = resolve; });
99
+ this.wake = undefined;
100
+ }
101
+ }
102
+ }
103
+ async consume() {
104
+ try {
105
+ for await (const event of this.handle) {
106
+ if ('sessionId' in event && event.sessionId)
107
+ this.sessionId = event.sessionId;
108
+ const correlated = { ...event, turnId: event.turnId ?? (event.type === 'result' || event.origin === 'background' ? undefined : this.active?.turn.id) };
109
+ // Observability callbacks must not terminate the agent or strand work.
110
+ try {
111
+ this.options.onEvent?.(correlated);
112
+ }
113
+ catch { /* host observer */ }
114
+ if (event.type === 'permission_request') {
115
+ this.observe({ type: 'permission_waiting', turnId: correlated.turnId, requestId: event.requestId,
116
+ toolUseId: event.toolUseId, toolName: event.toolName });
117
+ }
118
+ else if (event.type === 'permission_resolved') {
119
+ this.observe({ type: 'permission_resolved', turnId: correlated.turnId, requestId: event.requestId,
120
+ toolUseId: event.toolUseId, behavior: event.behavior });
121
+ }
122
+ if (event.type === 'result' && event.turnId !== undefined && this.active) {
123
+ if (event.turnId !== this.active.turn.id)
124
+ throw new Error('ccl-sdk: mismatched turn result');
125
+ const entry = this.active;
126
+ this.active = undefined;
127
+ this.observe({ type: 'turn_finished', turnId: entry.turn.id,
128
+ outcome: event.isError ? entry.cancelRequested ? 'interrupted' : 'failed' : 'succeeded',
129
+ ...(event.isError ? { reason: event.subtype } : {}) });
130
+ entry.resolve(event);
131
+ this.wake?.();
132
+ this.notifyDrained();
133
+ }
134
+ else if (event.type === 'error' && event.fatal) {
135
+ throw new Error(event.message);
136
+ }
137
+ else if (event.type === 'suspended') {
138
+ this.fail(new Error(`ccl-sdk: session suspended; resumeWithDecision(${event.sessionId}) is required`), event.origin === 'background' ? 'interrupted' : 'suspended');
139
+ break;
140
+ }
141
+ }
142
+ if (!this.closed)
143
+ this.fail(new Error('ccl-sdk: session transport ended'));
144
+ }
145
+ catch (error) {
146
+ this.fail(error);
147
+ }
148
+ finally {
149
+ await this.controls.close();
150
+ }
151
+ }
152
+ observe(event) {
153
+ try {
154
+ this.options.onLifecycle?.({ ...event, at: Date.now(), pid: this.pid, sessionId: this.sessionId,
155
+ pendingTurns: this.queue.length + (this.active ? 1 : 0) });
156
+ }
157
+ catch { /* host observer */ }
158
+ }
159
+ fail(reason, outcome = 'interrupted') {
160
+ if (this.closed)
161
+ return;
162
+ const error = reason instanceof Error ? reason : new Error(String(reason));
163
+ this.closed = true;
164
+ this.accepting = false;
165
+ const active = this.active;
166
+ this.active = undefined;
167
+ const queued = this.queue.splice(0);
168
+ if (active) {
169
+ this.observe({ type: 'turn_finished', turnId: active.turn.id, outcome, reason: error.message });
170
+ active.reject(error);
171
+ }
172
+ for (const entry of queued) {
173
+ this.observe({ type: 'turn_finished', turnId: entry.turn.id, outcome: 'cancelled', reason: error.message });
174
+ entry.reject(error);
175
+ }
176
+ this.observe({ type: 'session_closed', reason: error.message });
177
+ this.wake?.();
178
+ this.notifyDrained();
179
+ void this.controls.close().catch(() => { });
180
+ }
181
+ notifyDrained() {
182
+ if (this.active || this.queue.length)
183
+ return;
184
+ for (const resolve of this.drainWaiters.splice(0))
185
+ resolve();
186
+ }
187
+ async interrupt() {
188
+ if (this.active) {
189
+ if (!this.active.cancelRequested) {
190
+ this.active.cancelRequested = true;
191
+ this.observe({ type: 'turn_cancel_requested', turnId: this.active.turn.id });
192
+ }
193
+ await this.controls.interrupt();
194
+ }
195
+ }
196
+ async cancel(id) {
197
+ if (this.active?.turn.id === id)
198
+ return this.interrupt();
199
+ const index = this.queue.findIndex(entry => entry.turn.id === id);
200
+ if (index < 0)
201
+ return;
202
+ const entry = this.queue.splice(index, 1)[0];
203
+ this.observe({ type: 'turn_finished', turnId: id, outcome: 'cancelled', reason: 'Cancelled before dispatch' });
204
+ entry.reject(new Error('ccl-sdk: queued turn cancelled'));
205
+ this.notifyDrained();
206
+ }
207
+ /** Stop accepting inputs and drain accepted turns, keeping controls alive. */
208
+ async endInput() {
209
+ this.accepting = false;
210
+ if (!this.active && !this.queue.length)
211
+ return;
212
+ await new Promise(resolve => this.drainWaiters.push(resolve));
213
+ }
214
+ async close() {
215
+ this.fail(new Error('ccl-sdk: session closed'));
216
+ await this.controls.close();
217
+ await this.pumping;
218
+ }
219
+ }
220
+ export function createSession(options = {}) {
221
+ return new Session(options);
222
+ }
@@ -142,6 +142,8 @@ export type QueryOptions = {
142
142
  * suggestions; it is neither a result nor an instruction for the host to retry.
143
143
  */
144
144
  includeErrorDiagnostics?: boolean;
145
+ /** Opt-in finite, nonterminal gateway status events. Defaults to false. */
146
+ includeRequestProgress?: boolean;
145
147
  /**
146
148
  * slice sdk-json-schema-output (G4): a JSON Schema for structured output. When set, the CLI
147
149
  * validates the final result against it (retrying the model) and returns `structuredOutput` on
@@ -297,10 +299,63 @@ export type Usage = {
297
299
  * minimal set from the issue (P0-1) plus `permission_resolved` (audit trail)
298
300
  * and optional `raw` passthrough.
299
301
  */
300
- export type SDKEvent = {
302
+ export type SDKUserMessage = {
303
+ type: 'user';
304
+ message: {
305
+ role: 'user';
306
+ content: string | Array<{
307
+ type: string;
308
+ [key: string]: unknown;
309
+ }>;
310
+ };
311
+ uuid?: string;
312
+ parent_tool_use_id?: null;
313
+ session_id?: string;
314
+ };
315
+ export type SDKEvent = SDKEventPayload & {
316
+ turnId?: string;
317
+ origin?: 'user' | 'background';
318
+ };
319
+ /** Host observations, separate from model output and artifact verification.
320
+ * started means the input was written to the CLI, not that a model responded.
321
+ * Active interruptions can have external effects; they are not queued cancellations. */
322
+ export type SessionLifecycleEvent = {
323
+ at: number;
324
+ pid?: number;
325
+ sessionId?: string;
326
+ pendingTurns: number;
327
+ } & ({
328
+ type: 'turn_queued' | 'turn_started' | 'turn_cancel_requested';
329
+ turnId: string;
330
+ } | {
331
+ type: 'turn_finished';
332
+ turnId: string;
333
+ outcome: 'succeeded' | 'failed' | 'cancelled' | 'interrupted' | 'suspended';
334
+ reason?: string;
335
+ } | {
336
+ type: 'permission_waiting';
337
+ turnId?: string;
338
+ requestId: string;
339
+ toolUseId: string;
340
+ toolName: string;
341
+ } | {
342
+ type: 'permission_resolved';
343
+ turnId?: string;
344
+ requestId: string;
345
+ toolUseId: string;
346
+ behavior: 'allow' | 'deny';
347
+ } | {
348
+ type: 'session_closed';
349
+ reason: string;
350
+ });
351
+ type SDKEventPayload = {
301
352
  type: 'assistant_text';
302
353
  text: string;
303
354
  sessionId?: string;
355
+ } | {
356
+ type: 'request_progress';
357
+ progress: import('./requestProgress.js').GatewayRequestProgress;
358
+ sessionId?: string;
304
359
  } | {
305
360
  type: 'error_diagnostic';
306
361
  diagnostic: import('./errorDiagnostics.js').RecoveryDiagnostic;
@@ -452,3 +507,4 @@ export type Transport = {
452
507
  * rewritten). Absent for in-memory test transports. */
453
508
  pid?: number;
454
509
  };
510
+ export {};
@@ -100,6 +100,8 @@ Duo 让一个 Agent 执行任务,另一个独立检查结果。双方各有上
100
100
 
101
101
  本地修复候选在模型查询失败终止后将对应 Duo 任务置为暂停,保留材料和审查记录。先检查错误、凭据或模型可用性,再使用 `/duo resume`;失败不会被标为审核完成。
102
102
 
103
+ 2026-10-15 本地候选修复了超时后“显示已恢复、任务仍停在暂停阶段”的问题。恢复时根据当前要求、材料和有效审核记录重新确定工作阶段,再允许双方继续;暂停期间改过要求时,旧 PASS 不能恢复为可接受状态。此修复尚未发布。
104
+
103
105
  Duo 退出和任务完成不是一回事。任务被接受后 Duo 回到待命,可直接输入下一项任务;即使旧材料后来变化,仍可接新任务或使用 `/duo exit`。清理失败会显示原因,但不能锁住输入。普通 `/exit` 退出 CCL 会话。
104
106
 
105
107
  ### 必要时由用户裁决
@@ -0,0 +1,86 @@
1
+ # CLI installation and upgrades
2
+
3
+ For `1.4.5-rc.10`, use Node.js `>=22.19.0`. This matches the declared minimum of
4
+ the pinned `undici 8.0.2` runtime dependency; the earlier Node 20 declaration no
5
+ longer applies. macOS Node/Bun verification does not imply Windows verification.
6
+
7
+ Starting with `1.4.5-rc.10`, CCL declares `ccl` and `margay` as names for the same
8
+ `dist/cli.js`. Both names must report the package version and provide the same
9
+ options. The `1.4.5-rc.9` package declares only
10
+ `margay`. Installing that existing version will not add `ccl`.
11
+
12
+ ## Global and project installs
13
+
14
+ For a terminal command available outside a project, install the desired released
15
+ version globally:
16
+
17
+ ```sh
18
+ npm install --global @margay/ccl-core@latest
19
+ margay --version
20
+ ```
21
+
22
+ After installing a release with both entries, also run `ccl --version`. Both
23
+ outputs must match the installed package's version. Global installation does not
24
+ replace a CLI embedded in Margay Desktop or already loaded by a running process.
25
+
26
+ Without `--global`, npm installs a project dependency. Run that copy explicitly:
27
+
28
+ ```sh
29
+ npm install @margay/ccl-core@latest
30
+ ./node_modules/.bin/margay --version
31
+ ```
32
+
33
+ Releases with both entries also provide `./node_modules/.bin/ccl`. A successful
34
+ project install does not upgrade commands elsewhere on the shell's PATH.
35
+
36
+ ## Migrating old custom launchers (macOS/Linux)
37
+
38
+ Some earlier local deployments created shell scripts that pin a release path.
39
+ These are separate from npm-managed links. Inspect resolution before and after
40
+ upgrading:
41
+
42
+ ```sh
43
+ type -a ccl margay
44
+ npm prefix --global
45
+ npm ls --global @margay/ccl-core --depth=0
46
+ ```
47
+
48
+ There are two distinct cases:
49
+
50
+ - **npm reports `EEXIST`:** an unmanaged launcher occupies the intended npm bin
51
+ path. Inspect the named file and confirm it belongs to the old CCL deployment.
52
+ Move that specific file to an unused backup path, then repeat the installation.
53
+ Preserve unrelated executables; a blanket `--force` is not a migration plan.
54
+ - **Installation succeeds, but the command reports an old version:** an alias,
55
+ shell function or an earlier PATH directory is taking precedence. Inspect the
56
+ resolved entry. Back up the specific obsolete CCL wrapper, or update an explicit
57
+ alias/function to use the npm entry. Ensure the npm prefix's `bin` directory is
58
+ on PATH. Refresh the shell command cache (`rehash` in zsh, `hash -r` in bash),
59
+ then verify both commands again from outside the source checkout.
60
+
61
+ Keep each backup until the new commands work. To roll back a custom wrapper,
62
+ restore its saved bytes at the original path after inspecting what currently
63
+ occupies that path. For an npm-managed installation, install the previous exact
64
+ version. Future npm upgrades manage both links once the custom wrappers have
65
+ been migrated.
66
+
67
+ ## Checking a release tarball
68
+
69
+ From a built checkout, pack the release and test its actual npm installation:
70
+
71
+ ```sh
72
+ npm pack --ignore-scripts --pack-destination /path/to/artifacts
73
+ node tests/package-cli-install.mjs /path/to/artifacts/candidate.tgz /path/to/previous.tgz
74
+ ```
75
+
76
+ The smoke test uses isolated install prefixes, HOME/configuration and cache. It
77
+ checks fresh global and project installs, upgrade from the previous tarball,
78
+ both commands through PATH, matching executable/version/help, an unmanaged-file
79
+ collision, backup migration and PATH shadowing. Dependency lifecycle scripts are
80
+ disabled; this verifies CLI installation and startup, not native dependency or
81
+ model behavior. It currently runs on POSIX; Windows `.cmd`/PowerShell launchers
82
+ require a Windows run. It cleans its temporary installations even on failure.
83
+
84
+ Use the final tarball as the test input. A metadata edit after publishing rc.9
85
+ does not change rc.9 on npm; allocate a new version, rebuild, pack and verify
86
+ before publishing the corrected release.
@@ -0,0 +1,91 @@
1
+ # Completed-step checkpoints and background scheduling
2
+
3
+ For a long task, `ContextCheckpoint` can save selected completed tool results with
4
+ a summary and artifact references. Subsequent requests carry that summary and
5
+ retrieval references. Full original outputs, including image blocks, remain in
6
+ the session's transcript-adjacent `.checkpoints` directory with SHA-256 integrity
7
+ checks. `list` locates saved records; `restore` makes the original evidence
8
+ available again, including after transcript compaction or resume.
9
+
10
+ At a completed phase, omit `tool_use_ids` to save all eligible completed results.
11
+ For a subset, `list` returns available result IDs with their tool names and inputs.
12
+ Use the `checkpoint_id` returned by `save` or `list` when restoring; invented
13
+ protocol IDs are rejected.
14
+
15
+ The model chooses completed work; the tool rejects missing results, unresolved
16
+ permission interactions, errors, background launches, and duplicate archives.
17
+ User messages, corrections and user-supplied images are never projected away.
18
+ Checkpoint storage failures leave the incoming context intact. This path uses the
19
+ existing full compaction and result-budget machinery; it does not enable the
20
+ unimplemented provider cache-editing features. Checkpointing can change cache
21
+ reuse and adds tool calls, so savings depend on the actual task and provider.
22
+
23
+ `ccl daemon start --cwd /path/to/project` starts a detached local owner for the
24
+ project's existing `.ccl/scheduled_tasks.json`. The launching CLI may exit; the
25
+ owner remains. `ccl daemon run` is the foreground equivalent for an external
26
+ service manager. This command does not install a login or reboot service.
27
+
28
+ The owner uses the existing cron scheduler and project lock. It records an
29
+ occurrence before acknowledging it, then records execution and local delivery
30
+ separately under `.ccl/daemon/`. `status` shows these records. Completed output is
31
+ saved in `results/`; tool/events evidence is retained in `events/`. There is no
32
+ automatic email, chat or other external delivery.
33
+
34
+ ```sh
35
+ ccl daemon status --cwd /path/to/project
36
+ ccl daemon cancel OCCURRENCE_ID --cwd /path/to/project
37
+ ccl daemon approve OCCURRENCE_ID allow --cwd /path/to/project
38
+ ccl daemon approve OCCURRENCE_ID deny --cwd /path/to/project
39
+ ccl daemon run-missed OCCURRENCE_ID --cwd /path/to/project
40
+ ccl daemon deliver OCCURRENCE_ID --cwd /path/to/project
41
+ ccl daemon stop --cwd /path/to/project
42
+ ```
43
+
44
+ The CLI's existing permission rules apply. An unresolved permission suspends
45
+ execution until an explicit `approve` decision. Missed one-shot tasks are retained
46
+ as `missed` and need `run-missed`; recurring timing follows the shared scheduler.
47
+ On restart, accepted but not started work can continue. Work interrupted after
48
+ execution began is recorded as `interrupted` and never automatically replayed:
49
+ inspect its events and external effects before scheduling new work. `deliver`
50
+ retries saving an already successful result, without rerunning its tools.
51
+
52
+ `cancel` stops an occurrence; delete the durable cron definition as well when
53
+ future recurring occurrences should cease. A permission wait and an interrupted
54
+ run remain visible across restarts. Stop does not erase schedules or evidence.
55
+
56
+ When asking the assistant to schedule work, choose its execution scope:
57
+
58
+ - `CronCreate {execution: "session", afterMs: 60000, prompt: "…"}` keeps the
59
+ reminder in the current process. A one-shot print command exits after its turn,
60
+ so this scope does not promise execution after exit.
61
+ - `CronCreate {execution: "background", afterMs: 60000, prompt: "…"}` saves the
62
+ definition durably and starts or reuses the project's detached owner. Calendar
63
+ and anchored interval schedules use the same scope selection.
64
+ - Older calls with only `durable: true` still save the definition without starting
65
+ an owner. Explicit `execution: "session"` cannot be combined with durable storage.
66
+
67
+ The response separates `saved`, `status`, `owner.ready`, canonical `project`,
68
+ `nextRunAt`, `permissions`, and `resultDirectory`. `background_ready` requires a
69
+ live protocol response and ownership of the project scheduler lock. An interactive
70
+ scheduler yields that lock after its pending definition writes finish; private
71
+ session reminders continue in their own process.
72
+
73
+ If the response is `saved_not_running`, retain its task ID and use
74
+ `CronCreate {operation: "handoff", taskId: "…"}` to retry. This keeps the same
75
+ schedule and identity. Do not create a duplicate. A different CLI path or configuration
76
+ already running for the project is reported explicitly; inspect it and stop it
77
+ before handing off with the new configuration. Startup does not inherit the
78
+ creating terminal's permission bypass mode. Existing configured rules apply,
79
+ and unresolved actions remain `waiting_permission` until explicitly approved.
80
+
81
+ The owner uses the creating CLI path, model, configuration inputs and credential
82
+ environment. Protocol, multiplexer and test-session variables are removed. Launch
83
+ configuration is passed through a private pipe, without writing gateway credentials
84
+ into task or owner records. Restarting after a reboot requires starting an owner
85
+ again with valid configuration; no temporary credentials are promoted to permanent
86
+ configuration.
87
+
88
+ `CronList` includes background execution/delivery records and result locations,
89
+ even after a completed one-shot definition has been removed. `ccl daemon status`
90
+ provides the same records from a fresh CLI. Process liveness (`running`) and verified
91
+ scheduler readiness (`owner.ready`) are separate fields.
@@ -190,8 +190,60 @@ by endpoint switching.
190
190
  Output/context limits do not mean the account is out of credit. See the
191
191
  [recovery guide](./error-recovery-usage.md) for consent and external-blocker UX.
192
192
 
193
+ ## Request Deadlines And Slow Waits
194
+
195
+ The source candidate adds connection and response deadlines while preserving
196
+ the existing total request setup budget. These values are milliseconds:
197
+
198
+ | Variable | Default | Applies until |
199
+ | --- | --- | --- |
200
+ | `CCL_GATEWAY_REQUEST_TIMEOUT_MS` | `300000` | A response stream is established, including retry delays |
201
+ | `CCL_GATEWAY_CONNECT_TIMEOUT_MS` | Total setup budget | A pooled/new connection can send the request |
202
+ | `CCL_GATEWAY_RESPONSE_TIMEOUT_MS` | Total setup budget | Response headers arrive after sending |
203
+ | `CCL_GATEWAY_SLOW_WAIT_MS` | `30000` | A single slow-status notice becomes eligible |
204
+
205
+ Values must be whole numbers from 1 to 3600000. Invalid values fall back to the
206
+ default and produce a one-time stderr notice. A phase deadline cannot extend
207
+ the remaining total budget, and retries do not receive a new total budget.
208
+ Defaults therefore do not shorten the existing five-minute setup allowance.
209
+
210
+ For example, allow up to 30 seconds to connect and 180 seconds for headers:
211
+
212
+ ```sh
213
+ CCL_GATEWAY_CONNECT_TIMEOUT_MS=30000 CCL_GATEWAY_RESPONSE_TIMEOUT_MS=180000 margay
214
+ ```
215
+
216
+ After streaming starts, the separate model-progress idle watchdog applies;
217
+ the setup deadline does not impose a maximum duration on healthy output.
218
+ The TUI shows the current request phase and a slow-wait notice. Escape cancels
219
+ through the existing task cancellation path. Status notices do not reset the
220
+ model-progress watchdog or become assistant text.
221
+
222
+ A local timeout reports `gateway_request_timeout` with the observed phase.
223
+ It cannot establish whether the remote request completed. Check task state
224
+ before repeating work that may have side effects. SDK hosts can opt into
225
+ [structured request status](../sdk/README.md#gateway-request-status-source-candidate).
226
+
193
227
  ## Troubleshooting
194
228
 
229
+ In the source candidate, headless runs with `CI=1` can authenticate using an
230
+ admitted gateway configuration alone. A separate Anthropic API key is not
231
+ required for that route. Missing or rejected gateway credentials still require
232
+ a valid login or another supported credential source. This fixes a startup
233
+ failure present in rc.9; it does not change the gateway's 401 handling.
234
+
235
+ The source candidate also reports a failed in-process teammate request to its
236
+ team leader with the visible failure reason. The teammate remains available for
237
+ explicit new work after the failure; a timeout does not automatically retry the
238
+ task or mark it complete. Check any uncertain side effects before resuming.
239
+
240
+ In the source candidate, headless background completions belong to the input
241
+ that launched them. They share that input's `--max-turns` ceiling and cannot
242
+ restart it after an error or interrupt. Successful background work is collected
243
+ before its final result. A new SDK input can start fresh work; late notifications
244
+ from the previous input do not start model calls for it. Closing an SDK query
245
+ also waits for background shell termination during the CLI's bounded shutdown.
246
+
195
247
  | Symptom | Action |
196
248
  | --- | --- |
197
249
  | Website offers no login entry | Confirm that login is enabled for that website/environment; a 200 page alone does not prove it is enabled. Cancel the CLI wait if disabled |
@@ -0,0 +1,50 @@
1
+ # 1.4.5-rc.10
2
+
3
+ 本版本包含双命令入口和 Mac Phase B 稳定性修复。包同时注册 `ccl` 和
4
+ `margay`,两个命令指向同一份运行程序,功能、参数和版本相同。
5
+ CLI 与 SDK 随同一个 `@margay/ccl-core` 包交付。
6
+
7
+ ## 相对 rc.9 的更新
8
+
9
+ - 网关请求区分连接、等待响应和流式读取阶段,阶段期限共用请求总期限。
10
+ CLI/SDK 可选择接收等待进度;通知异常不阻塞请求,不混入模型上下文。
11
+ - 无头执行的一次输入只返回一个终态。任务失败后,旧后台通知不会重新唤起模型、
12
+ 执行工具或重复返回结果;取消及 SDK 关闭会清理所属后台任务。
13
+ - 自动继续与压缩累计使用剩余任务预算,避免重置额度后不断运行。
14
+ - 修复 CI 网关凭据识别、Duo 超时后的恢复,以及进程内/独立进程 Swarm 的失败通知。
15
+ - 保留 rc.9 的输出预算、慢请求等待和有界断流恢复;已完成工具结果继续保留。
16
+ - 将外部运行依赖 `undici` 固定为本候选验证过的 `8.0.2`,避免全新 npm 安装
17
+ 自动选择不同版本。Node 声明要求调整为 `>=22.19.0`,与该依赖声明一致;
18
+ 原先的 Node 20 支持声明不再适用。
19
+
20
+ ## 安装与升级
21
+
22
+ 全局安装后核对两个入口:
23
+
24
+ ```sh
25
+ npm install --global @margay/ccl-core@1.4.5-rc.10
26
+ ccl --version
27
+ margay --version
28
+ ```
29
+
30
+ 两者应输出 `1.4.5-rc.10 (Margay)`。不带 `--global` 的安装只更新项目依赖,
31
+ 可通过 `./node_modules/.bin/ccl` 或 `./node_modules/.bin/margay` 使用。
32
+
33
+ 旧的自定义启动脚本、alias 或 PATH 排序可能继续选择旧版本。若安装报告
34
+ `EEXIST` 或运行版本不一致,按[安装与升级说明](../cli-installation.md)检查并
35
+ 备份迁移对应入口。npm 全局安装不会替换 Margay Desktop 内嵌的运行包。
36
+
37
+ ## 验证与已知边界
38
+
39
+ Phase B 候选已在 Mac 验证 Node/Bun 下 CLI、SDK 的成功、超时、取消和后续输入,
40
+ 以及 TUI、Duo、Swarm 和 Margay.app 私有副本的消费链路。CLI 构建 SHA256:
41
+ `92a070720228fe35b80608e751fb59ea0116ac7c9e9115c009da98e1063a39df`。
42
+ 包的安装检查另外验证 POSIX 全新全局/项目安装、rc.9 升级、双入口、旧脚本冲突和 PATH 遮挡。
43
+
44
+ - 本版不包含“轮次耗尽返回退出码 2,由上层自动续接”的新方案;到上限仍报告
45
+ `error_max_turns`。真实视频验证的首个 48 轮调用失败,另一次明确输入才完成交付。
46
+ - DeepSeek thinking 400 的根因及针对性修复尚未收口;不能承诺本版消除该错误,
47
+ 也不能仅凭 `blocked_external` 认定连接/超时问题已修复。
48
+ - Windows 实机未验证;全仓 typecheck 的 11 条既有诊断未解决。
49
+ - 获批 reactive compact 与 taskBudget 组合在当前公开入口不可达,仅作源码核对。
50
+ 运营接口 401 及 shell 失败文案与 `is_error:false` 不一致的线索仍待定位。