@gajae-code/coding-agent 0.18.0 → 0.18.1
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/CHANGELOG.md +35 -0
- package/dist/types/config/settings-schema.d.ts +2 -2
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +1 -0
- package/dist/types/sdk/acp/adapter.d.ts +3 -1
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
- package/dist/types/sdk/client/client.d.ts +5 -1
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/task/index.d.ts +0 -1
- package/dist/types/tools/descriptors.d.ts +5 -0
- package/dist/types/tools/eval-backends.d.ts +37 -0
- package/dist/types/tools/eval.d.ts +1 -6
- package/dist/types/tools/index.d.ts +1 -36
- package/dist/types/tools/session-descriptions.d.ts +22 -0
- package/package.json +7 -7
- package/scripts/generate-sdk-operation-inventory.ts +2 -0
- package/src/cli/doctor/managed-link.ts +4 -4
- package/src/cli/doctor/repairs.ts +9 -1
- package/src/config/settings-schema.ts +8 -2
- package/src/exec/bash-shell-guardian.ts +31 -15
- package/src/gjc-runtime/session-state-sidecar.ts +88 -58
- package/src/internal-urls/docs-index.generated.ts +4 -4
- package/src/modes/acp/acp-agent.ts +97 -16
- package/src/modes/interactive-mode.ts +10 -0
- package/src/modes/utils/context-usage.ts +2 -6
- package/src/prompts/tools/task.md +8 -27
- package/src/sdk/acp/adapter.ts +38 -3
- package/src/sdk/broker/doctor-restart.ts +38 -3
- package/src/sdk/bus/index.ts +3 -3
- package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
- package/src/sdk/client/client.ts +53 -35
- package/src/sdk/protocol/operation-inventory.generated.json +11 -0
- package/src/session/agent-session.ts +65 -15
- package/src/task/index.ts +21 -66
- package/src/tools/descriptors.ts +27 -1
- package/src/tools/eval-backends.ts +95 -0
- package/src/tools/eval.ts +3 -15
- package/src/tools/index.ts +9 -88
- package/src/tools/search.ts +2 -7
- package/src/tools/session-descriptions.ts +135 -0
- package/src/tools/tool-catalog.generated.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,41 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.18.1] - 2026-09-29
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- `./install.sh --dev` and `gjc doctor` no longer treat this checkout's own `gjc` and `가재씨` links as foreign after a macOS reboot. The ownership receipt still binds the link by inode, but no longer compares the recorded `st_dev`, which APFS changes across boots. A receipt left behind after its link was removed by hand is recognized as this checkout's and replaced, while a receipt that names another checkout is refused before either link is touched.
|
|
10
|
+
|
|
11
|
+
- Loading `search` or `eval` for the first time no longer changes its advertised tool description, so the provider-visible `tools` block and the prompt-cache prefix stay stable. Before the implementation loads, the description is now rendered from the session (hashline or line-number display for `search`, the allowed Python/JavaScript backends for `eval`), instead of taking the stub-session text from the generated catalog.
|
|
12
|
+
|
|
13
|
+
- Keep the provider-visible `task` description stable before and after the tool loads by rendering it from session settings without listing configured agent names; unknown-agent errors continue to list all visible agents. IRC guidance is now independent of whether the IRC tool becomes available mid-session.
|
|
14
|
+
|
|
15
|
+
- Windows binary update via exactReplaceRetained now succeeds with owner-only ACL enforcement. The retained update path required READ_CONTROL on both the staged source and current destination handles to query ACL ownership, but the handles were opened without READ_CONTROL, causing every real Windows update to fail with acl_unavailable. (#6096)
|
|
16
|
+
|
|
17
|
+
- Reduced macOS idle CPU from the bash shell guardian: the Darwin ancestry poll now reads kernel process identities first and validates only new descendants with `Process.fromPid`, instead of querying every process on each scan. A descendant whose validation fails transiently is retried on the next poll rather than dropped from cleanup.
|
|
18
|
+
|
|
19
|
+
- User-cancelled turns (ACP session/cancel, SDK turn.abort) settle as `cancelled` again instead of a failed terminal (regression from 0.18.0).
|
|
20
|
+
|
|
21
|
+
- ACP cancels received before SDK prompt admission now stay attached to the waiter: the prompt is not dispatched, or a real terminal abort is retried after admission, rather than letting the turn continue.
|
|
22
|
+
|
|
23
|
+
- ACP MCP launch failures now retain the broker lifecycle cause and a bounded, credential-free diagnostic, while SDK transport failures keep their original retry code.
|
|
24
|
+
|
|
25
|
+
- `gjc doctor --fix --repair service.restart-owned` on the SDK broker now reports a broker refusal, such as `restart_busy` while any session is live, as `blocked` with reason `prepare_refused:<code>` and no side effect. It was previously reported as `uncertain` / `repair_execution_unverified` with `sideEffectStarted: true`. A refused commit now also cancels its prepared reservation instead of leaving new work refused until the lease expires.
|
|
26
|
+
|
|
27
|
+
- Treat cooperative SDK prompt pauses as successful terminal stops instead of unreported failures.
|
|
28
|
+
|
|
29
|
+
- A displayed extension message sent from `session_start` (such as a handoff summary) now appears once at startup instead of twice, and startup notices shown before the first paint are no longer wiped. Transcript rebuilds requested before the initial transcript paint are skipped; the first paint renders the full session.
|
|
30
|
+
|
|
31
|
+
### Documentation
|
|
32
|
+
|
|
33
|
+
- Updated: Clarified that human reviewers only need to approve the current head on GitHub; body verdict lines are required only for agent reviewers and owner self-approval.
|
|
34
|
+
|
|
35
|
+
### Bug fixes
|
|
36
|
+
|
|
37
|
+
- Fixed: Human approval now works when a PR has no verdict line in its body. GJC checks GitHub reviews for human approval on the current head instead.
|
|
38
|
+
- Fixed: A valid PR contract with no approval yet now shows a "Waiting for approval" notice instead of an error.
|
|
39
|
+
|
|
5
40
|
## [0.18.0] - 2026-09-27
|
|
6
41
|
|
|
7
42
|
### Added
|
|
@@ -2183,11 +2183,11 @@ export declare const SETTINGS_SCHEMA: {
|
|
|
2183
2183
|
readonly ui: {
|
|
2184
2184
|
readonly tab: "context";
|
|
2185
2185
|
readonly label: "Compaction Threshold";
|
|
2186
|
-
readonly description: "Percent threshold for context maintenance;
|
|
2186
|
+
readonly description: "Percent threshold for context maintenance; the 300,000-token default cap applies only with adaptive mode disabled (except during context promotion)";
|
|
2187
2187
|
readonly options: readonly [{
|
|
2188
2188
|
readonly value: "default";
|
|
2189
2189
|
readonly label: "Default";
|
|
2190
|
-
readonly description: "
|
|
2190
|
+
readonly description: "300,000-token default cap applies only with adaptive mode disabled (except during context promotion)";
|
|
2191
2191
|
}, {
|
|
2192
2192
|
readonly value: "10";
|
|
2193
2193
|
readonly label: "10%";
|
|
@@ -39,6 +39,7 @@ export declare const __sessionStateSidecarTestHooks: {
|
|
|
39
39
|
beforeRescopeJournalWrite?: (cwd: string) => void | Promise<void>;
|
|
40
40
|
beforePersistFromEvent?: (eventType: string, cwd: string) => void | Promise<void>;
|
|
41
41
|
beforeRescopePublish?: () => void | Promise<void>;
|
|
42
|
+
disableCoordinatorFileSyncForTests?: boolean;
|
|
42
43
|
};
|
|
43
44
|
interface RuntimeStateEvent {
|
|
44
45
|
type: string;
|
|
@@ -61,7 +61,9 @@ export declare class AcpSdkAdapterError extends Error {
|
|
|
61
61
|
}
|
|
62
62
|
/**
|
|
63
63
|
* The error an ACP session launch must throw once a lifecycle request that carried MCP
|
|
64
|
-
* servers has failed.
|
|
64
|
+
* servers has failed. Broker transport failures from the SDK client are preserved verbatim;
|
|
65
|
+
* other failures keep the adapter's `unavailable` code while carrying a bounded, credential-free
|
|
66
|
+
* copy of the original code and message as their cause diagnostic.
|
|
65
67
|
*/
|
|
66
68
|
export declare function acpMcpLaunchFailure(error: unknown, mcpServers: SessionLifecycleMcpServer[]): unknown;
|
|
67
69
|
export type AcpReconnectFailedHandler = (error: SdkClientError) => void;
|
|
@@ -292,7 +292,7 @@ export declare const SDK_LIFECYCLE_ROUTER_PROTOCOL_VERSION = 1;
|
|
|
292
292
|
* Generation 193 gates streamed content on negotiated observer capabilities,
|
|
293
293
|
* so existing owners are replaced before exposing the new observer path.
|
|
294
294
|
*/
|
|
295
|
-
export declare const DAEMON_GENERATION =
|
|
295
|
+
export declare const DAEMON_GENERATION = 197;
|
|
296
296
|
/**
|
|
297
297
|
* Serving-compatibility boundary for daemon lifecycle requests. Epoch 7
|
|
298
298
|
* requires durable publication receipts and acknowledged attachment dispatch,
|
|
@@ -2,6 +2,8 @@ export type SdkErrorCode = "invalid_input" | "unknown_operation" | "not_found" |
|
|
|
2
2
|
export declare class SdkClientError extends Error {
|
|
3
3
|
readonly code: SdkErrorCode;
|
|
4
4
|
readonly details: unknown;
|
|
5
|
+
/** True only when the SDK client itself reported a transport failure, never for a server error frame. */
|
|
6
|
+
readonly transport: boolean;
|
|
5
7
|
/**
|
|
6
8
|
* Reconnect-cycle diagnostics, separate from `details` because `details` is an
|
|
7
9
|
* established contract: callers read the terminating transport error straight off
|
|
@@ -10,7 +12,9 @@ export declare class SdkClientError extends Error {
|
|
|
10
12
|
* break every such reader, so the new attribution rides alongside it instead.
|
|
11
13
|
*/
|
|
12
14
|
readonly reconnect?: SdkReconnectExhaustedDetails;
|
|
13
|
-
constructor(code: SdkErrorCode, message: string, details?: unknown, reconnect?: SdkReconnectExhaustedDetails
|
|
15
|
+
constructor(code: SdkErrorCode, message: string, details?: unknown, reconnect?: SdkReconnectExhaustedDetails, options?: {
|
|
16
|
+
transport?: boolean;
|
|
17
|
+
});
|
|
14
18
|
}
|
|
15
19
|
/** A prepared transport operation failed before any wire handoff. */
|
|
16
20
|
export declare class SdkPreparedDispatchError extends SdkClientError {
|
|
@@ -1940,6 +1940,8 @@ export declare class AgentSession {
|
|
|
1940
1940
|
* Get session statistics.
|
|
1941
1941
|
*/
|
|
1942
1942
|
getSessionStats(): SessionStats;
|
|
1943
|
+
/** Current auto-compaction threshold, including active model-promotion policy. */
|
|
1944
|
+
getAutoCompactionThresholdTokens(contextTokens?: number): number;
|
|
1943
1945
|
/**
|
|
1944
1946
|
* Get current context usage statistics.
|
|
1945
1947
|
* Uses the last assistant message's usage data when available,
|
|
@@ -51,7 +51,6 @@ export declare class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskT
|
|
|
51
51
|
*/
|
|
52
52
|
get parameters(): TaskToolSchemaInstance;
|
|
53
53
|
renderCall(args: unknown, options: Parameters<typeof renderTaskCall>[1], theme: Theme): import("@gajae-code/tui").Component;
|
|
54
|
-
/** Dynamic description that reflects current disabled-agent settings */
|
|
55
54
|
get description(): string;
|
|
56
55
|
private constructor();
|
|
57
56
|
/**
|
|
@@ -17,6 +17,11 @@ export interface ToolDescriptorMetadata {
|
|
|
17
17
|
readonly intent?: AgentTool<any, any, any>["intent"];
|
|
18
18
|
readonly parametersForSession?: (session?: ToolSession) => TSchema;
|
|
19
19
|
readonly parameters?: TSchema;
|
|
20
|
+
/**
|
|
21
|
+
* Renders the description the loaded tool will report for this session. Tools whose prompt text
|
|
22
|
+
* depends on session settings need it, or the advertised text changes on first load (#5992).
|
|
23
|
+
*/
|
|
24
|
+
readonly descriptionForSession?: (session: ToolSession) => string;
|
|
20
25
|
readonly description?: string;
|
|
21
26
|
readonly label?: string;
|
|
22
27
|
readonly customWireName?: string;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ToolSession } from ".";
|
|
2
|
+
export interface EvalBackendsAllowance {
|
|
3
|
+
python: boolean;
|
|
4
|
+
js: boolean;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Parse the `GJC_PY` multi-value token into per-backend booleans.
|
|
8
|
+
*
|
|
9
|
+
* Tokens (case-insensitive):
|
|
10
|
+
* - `0` / `bash` → JavaScript only (`{ py: false, js: true }`)
|
|
11
|
+
* - `1` / `py` → Python only (`{ py: true, js: false }`)
|
|
12
|
+
* - `js` → JavaScript only (`{ py: false, js: true }`)
|
|
13
|
+
* - `mix` / `both` → both backends (`{ py: true, js: true }`)
|
|
14
|
+
*
|
|
15
|
+
* Returns `null` when `GJC_PY` is unset, empty, or holds an unrecognized
|
|
16
|
+
* token, so the caller can fall back to the legacy `PI_PY` / `PI_JS` flags or
|
|
17
|
+
* per-key settings. This matches the documented contract that invalid values
|
|
18
|
+
* are ignored.
|
|
19
|
+
*/
|
|
20
|
+
export declare function parseGjcPy(env: Record<string, string | undefined>): {
|
|
21
|
+
py: boolean;
|
|
22
|
+
js: boolean;
|
|
23
|
+
} | null;
|
|
24
|
+
/**
|
|
25
|
+
* Resolve eval-backend allowance from environment only. `GJC_PY` wins when set
|
|
26
|
+
* to a recognized token; otherwise the legacy `PI_PY` / `PI_JS` flags apply.
|
|
27
|
+
* Returns `null` when no env override is set so the caller can defer to settings.
|
|
28
|
+
*/
|
|
29
|
+
export declare function resolveEvalBackendsFromEnv(env: Record<string, string | undefined>): EvalBackendsAllowance | null;
|
|
30
|
+
/** Read per-backend allowance from settings (defaults true). */
|
|
31
|
+
export declare function readEvalBackendsAllowance(session: ToolSession): EvalBackendsAllowance;
|
|
32
|
+
/**
|
|
33
|
+
* Materialize the active eval backend allowance. `GJC_PY` takes precedence
|
|
34
|
+
* over the legacy `PI_PY` / `PI_JS` env flags, which in turn override the
|
|
35
|
+
* per-key settings (defaults true). When no env override is set, settings win.
|
|
36
|
+
*/
|
|
37
|
+
export declare function resolveEvalBackends(session: ToolSession): EvalBackendsAllowance;
|
|
@@ -42,11 +42,7 @@ export type EvalToolResult = {
|
|
|
42
42
|
details: EvalToolDetails | undefined;
|
|
43
43
|
};
|
|
44
44
|
export type EvalProxyExecutor = (params: EvalToolParams, signal?: AbortSignal) => Promise<EvalToolResult>;
|
|
45
|
-
export
|
|
46
|
-
py?: boolean;
|
|
47
|
-
js?: boolean;
|
|
48
|
-
}
|
|
49
|
-
export declare function getEvalToolDescription(options?: EvalToolDescriptionOptions): string;
|
|
45
|
+
export { type EvalToolDescriptionOptions, getEvalToolDescription } from "./session-descriptions";
|
|
50
46
|
export interface EvalToolOptions {
|
|
51
47
|
proxyExecutor?: EvalProxyExecutor;
|
|
52
48
|
}
|
|
@@ -105,4 +101,3 @@ export declare const evalToolRenderer: {
|
|
|
105
101
|
mergeCallAndResult: boolean;
|
|
106
102
|
inline: boolean;
|
|
107
103
|
};
|
|
108
|
-
export {};
|
|
@@ -405,42 +405,7 @@ export interface BuiltinCapabilityCatalogEntry {
|
|
|
405
405
|
}
|
|
406
406
|
export declare const BUILTIN_CAPABILITY_CATALOG: readonly BuiltinCapabilityCatalogEntry[];
|
|
407
407
|
export type ToolName = keyof typeof BUILTIN_TOOLS;
|
|
408
|
-
export
|
|
409
|
-
python: boolean;
|
|
410
|
-
js: boolean;
|
|
411
|
-
}
|
|
412
|
-
/**
|
|
413
|
-
* Parse the `GJC_PY` multi-value token into per-backend booleans.
|
|
414
|
-
*
|
|
415
|
-
* Tokens (case-insensitive):
|
|
416
|
-
* - `0` / `bash` → JavaScript only (`{ py: false, js: true }`)
|
|
417
|
-
* - `1` / `py` → Python only (`{ py: true, js: false }`)
|
|
418
|
-
* - `js` → JavaScript only (`{ py: false, js: true }`)
|
|
419
|
-
* - `mix` / `both` → both backends (`{ py: true, js: true }`)
|
|
420
|
-
*
|
|
421
|
-
* Returns `null` when `GJC_PY` is unset, empty, or holds an unrecognized
|
|
422
|
-
* token, so the caller can fall back to the legacy `PI_PY` / `PI_JS` flags or
|
|
423
|
-
* per-key settings. This matches the documented contract that invalid values
|
|
424
|
-
* are ignored.
|
|
425
|
-
*/
|
|
426
|
-
export declare function parseGjcPy(env: Record<string, string | undefined>): {
|
|
427
|
-
py: boolean;
|
|
428
|
-
js: boolean;
|
|
429
|
-
} | null;
|
|
430
|
-
/**
|
|
431
|
-
* Resolve eval-backend allowance from environment only. `GJC_PY` wins when set
|
|
432
|
-
* to a recognized token; otherwise the legacy `PI_PY` / `PI_JS` flags apply.
|
|
433
|
-
* Returns `null` when no env override is set so the caller can defer to settings.
|
|
434
|
-
*/
|
|
435
|
-
export declare function resolveEvalBackendsFromEnv(env: Record<string, string | undefined>): EvalBackendsAllowance | null;
|
|
436
|
-
/** Read per-backend allowance from settings (defaults true). */
|
|
437
|
-
export declare function readEvalBackendsAllowance(session: ToolSession): EvalBackendsAllowance;
|
|
438
|
-
/**
|
|
439
|
-
* Materialize the active eval backend allowance. `GJC_PY` takes precedence
|
|
440
|
-
* over the legacy `PI_PY` / `PI_JS` env flags, which in turn override the
|
|
441
|
-
* per-key settings (defaults true). When no env override is set, settings win.
|
|
442
|
-
*/
|
|
443
|
-
export declare function resolveEvalBackends(session: ToolSession): EvalBackendsAllowance;
|
|
408
|
+
export { type EvalBackendsAllowance, parseGjcPy, readEvalBackendsAllowance, resolveEvalBackends, resolveEvalBackendsFromEnv, } from "./eval-backends";
|
|
444
409
|
/**
|
|
445
410
|
* Create tools from the descriptor registry.
|
|
446
411
|
*/
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type TaskToolSchemaInstance } from "../task/types";
|
|
2
|
+
interface DescriptionSettings {
|
|
3
|
+
get(key: "task.maxConcurrency" | "task.isolation.mode" | "task.simple" | "task.disabledAgents" | "eval.py" | "eval.js" | "readLineNumbers" | "readHashLines" | "edit.mode"): unknown;
|
|
4
|
+
getEffectiveAutorouting(): {
|
|
5
|
+
active: boolean;
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
interface DescriptionSession {
|
|
9
|
+
settings: DescriptionSettings;
|
|
10
|
+
getSessionSpawns?: () => string | null;
|
|
11
|
+
hasEditTool?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface EvalToolDescriptionOptions {
|
|
14
|
+
py?: boolean;
|
|
15
|
+
js?: boolean;
|
|
16
|
+
}
|
|
17
|
+
export declare function getEvalToolDescription(options?: EvalToolDescriptionOptions): string;
|
|
18
|
+
export declare function evalToolDescriptionForSession(session: DescriptionSession | null | undefined): string;
|
|
19
|
+
export declare function searchToolDescriptionForSession(session: DescriptionSession): string;
|
|
20
|
+
export declare function renderTaskDescription(session: DescriptionSession): string;
|
|
21
|
+
export declare function taskParametersForSession(session?: DescriptionSession): TaskToolSchemaInstance;
|
|
22
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@gajae-code/coding-agent",
|
|
4
|
-
"version": "0.18.
|
|
4
|
+
"version": "0.18.1",
|
|
5
5
|
"description": "Gajae Code CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://gajae-code.com",
|
|
7
7
|
"author": "Yeachan-Heo and Gajae Code Contributors",
|
|
@@ -57,12 +57,12 @@
|
|
|
57
57
|
"@agentclientprotocol/sdk": "1.3.0",
|
|
58
58
|
"@babel/parser": "^7.29.3",
|
|
59
59
|
"@mozilla/readability": "^0.6.0",
|
|
60
|
-
"@gajae-code/stats": "0.18.
|
|
61
|
-
"@gajae-code/agent-core": "0.18.
|
|
62
|
-
"@gajae-code/ai": "0.18.
|
|
63
|
-
"@gajae-code/natives": "0.18.
|
|
64
|
-
"@gajae-code/tui": "0.18.
|
|
65
|
-
"@gajae-code/utils": "0.18.
|
|
60
|
+
"@gajae-code/stats": "0.18.1",
|
|
61
|
+
"@gajae-code/agent-core": "0.18.1",
|
|
62
|
+
"@gajae-code/ai": "0.18.1",
|
|
63
|
+
"@gajae-code/natives": "0.18.1",
|
|
64
|
+
"@gajae-code/tui": "0.18.1",
|
|
65
|
+
"@gajae-code/utils": "0.18.1",
|
|
66
66
|
"@puppeteer/browsers": "^2.13.0",
|
|
67
67
|
"@types/turndown": "5.0.6",
|
|
68
68
|
"@xterm/headless": "^6.0.0",
|
|
@@ -203,6 +203,8 @@ const LOCKED_EXCLUSIONS: Readonly<Record<string, string>> = {
|
|
|
203
203
|
"agent_session:queueDeferredMessage": "internal accessor/plumbing, not a user-facing control seam",
|
|
204
204
|
"agent_session:queueDeferredMessageForTests": "internal accessor/plumbing, not a user-facing control seam",
|
|
205
205
|
"agent_session:getContextUsageObservabilityForTests": "test-only observability, not a user-facing control seam",
|
|
206
|
+
"agent_session:getAutoCompactionThresholdTokens":
|
|
207
|
+
"internal compaction-threshold accessor feeding the /context breakdown; exposed through context.get, not an independent SDK operation",
|
|
206
208
|
"agent_session:purgeQueuedCustomMessages": "internal accessor/plumbing, not a user-facing control seam",
|
|
207
209
|
"agent_session:clearQueue": "internal accessor/plumbing, not a user-facing control seam",
|
|
208
210
|
"agent_session:popLastQueuedMessage": "internal accessor/plumbing, not a user-facing control seam",
|
|
@@ -217,11 +217,11 @@ export async function describeManagedLink(
|
|
|
217
217
|
const parent = await fs.lstat(parentPath, { bigint: true }).catch(() => undefined);
|
|
218
218
|
const trustedReceipt =
|
|
219
219
|
!!body && ownedReceipt(receipt) && publicCandidates.some(candidate => candidate.path === body.source);
|
|
220
|
+
// The recorded st_dev is not compared: macOS APFS gives the same volume a new st_dev after a reboot,
|
|
221
|
+
// which made every pre-reboot receipt look foreign (#5990). The inode binds the link, and the volume
|
|
222
|
+
// is re-derived from the parent directory observed in the same snapshot.
|
|
220
223
|
const identityMatches =
|
|
221
|
-
!!body &&
|
|
222
|
-
!!link &&
|
|
223
|
-
body.identity.dev === link.stat.dev.toString() &&
|
|
224
|
-
body.identity.ino === link.stat.ino.toString();
|
|
224
|
+
!!body && !!link && body.identity.ino === link.stat.ino.toString() && link.stat.dev === link.parent.dev;
|
|
225
225
|
const owned = trustedReceipt && (link ? identityMatches : occupant === undefined);
|
|
226
226
|
const recordedCandidate = body && publicCandidates.find(candidate => candidate.path === body.source);
|
|
227
227
|
let status: ManagedLinkDescriptor["status"] = "unknown";
|
|
@@ -892,7 +892,15 @@ export async function executeSelectedDoctorRepair(
|
|
|
892
892
|
outcome =
|
|
893
893
|
result.kind === "restarted"
|
|
894
894
|
? { kind: result.kind, successorIncarnation: result.successor.incarnation }
|
|
895
|
-
: {
|
|
895
|
+
: {
|
|
896
|
+
kind: result.kind,
|
|
897
|
+
// A broker refusal carries the broker's own code (e.g. restart_busy).
|
|
898
|
+
...("reason" in result
|
|
899
|
+
? { reason: result.reason }
|
|
900
|
+
: "code" in result
|
|
901
|
+
? { reason: result.code }
|
|
902
|
+
: {}),
|
|
903
|
+
};
|
|
896
904
|
} else {
|
|
897
905
|
settings = await Settings.loadForScope({ cwd: context.cwd, agentDir: context.agentRoot.locator });
|
|
898
906
|
const result = await restartDaemonForDoctor({
|
|
@@ -2050,9 +2050,15 @@ export const SETTINGS_SCHEMA = {
|
|
|
2050
2050
|
ui: {
|
|
2051
2051
|
tab: "context",
|
|
2052
2052
|
label: "Compaction Threshold",
|
|
2053
|
-
description:
|
|
2053
|
+
description:
|
|
2054
|
+
"Percent threshold for context maintenance; the 300,000-token default cap applies only with adaptive mode disabled (except during context promotion)",
|
|
2054
2055
|
options: [
|
|
2055
|
-
{
|
|
2056
|
+
{
|
|
2057
|
+
value: "default",
|
|
2058
|
+
label: "Default",
|
|
2059
|
+
description:
|
|
2060
|
+
"300,000-token default cap applies only with adaptive mode disabled (except during context promotion)",
|
|
2061
|
+
},
|
|
2056
2062
|
{ value: "10", label: "10%", description: "Extremely early maintenance" },
|
|
2057
2063
|
{ value: "20", label: "20%", description: "Very early maintenance" },
|
|
2058
2064
|
{ value: "30", label: "30%", description: "Early maintenance" },
|
|
@@ -175,18 +175,28 @@ function createDarwinAncestryTracker(owned: Map<string, NativeProcess>): DarwinA
|
|
|
175
175
|
},
|
|
176
176
|
});
|
|
177
177
|
const knownUniqueIds = new Set<bigint>();
|
|
178
|
+
// Ancestry evidence (knownUniqueIds) and successful retention are tracked
|
|
179
|
+
// separately: a descendant whose process validation fails stays known, so its
|
|
180
|
+
// own children still chain, but it is retried on every poll until retained.
|
|
181
|
+
const retainedUniqueIds = new Set<bigint>();
|
|
178
182
|
const { Process } = require("@gajae-code/natives") as NativeProcessBindings;
|
|
183
|
+
// Reuse buffer across poll iterations to avoid allocations inside the loop.
|
|
184
|
+
const identityBuffer = new Uint8Array(56);
|
|
185
|
+
const identityView = new DataView(identityBuffer.buffer);
|
|
179
186
|
const uniqueIdentity = (pid: number): { uniqueId: bigint; parentUniqueId: bigint } | undefined => {
|
|
180
|
-
const
|
|
181
|
-
if (
|
|
182
|
-
|
|
183
|
-
|
|
187
|
+
const bytes = proc.symbols.proc_pidinfo(pid, 17, 0, ptr(identityBuffer), identityBuffer.byteLength);
|
|
188
|
+
if (bytes !== identityBuffer.byteLength) return undefined;
|
|
189
|
+
return {
|
|
190
|
+
uniqueId: identityView.getBigUint64(16, true),
|
|
191
|
+
parentUniqueId: identityView.getBigUint64(24, true),
|
|
192
|
+
};
|
|
184
193
|
};
|
|
185
194
|
const track = (processRef: NativeProcess, signedUniqueId?: bigint): boolean => {
|
|
186
195
|
const identity = signedUniqueId === undefined ? uniqueIdentity(processRef.pid) : undefined;
|
|
187
196
|
const uniqueId = signedUniqueId ?? identity?.uniqueId;
|
|
188
197
|
if (uniqueId === undefined) return false;
|
|
189
198
|
knownUniqueIds.add(uniqueId);
|
|
199
|
+
retainedUniqueIds.add(uniqueId);
|
|
190
200
|
retainOwnedProcess(owned, processRef);
|
|
191
201
|
return true;
|
|
192
202
|
};
|
|
@@ -201,23 +211,29 @@ function createDarwinAncestryTracker(owned: Map<string, NativeProcess>): DarwinA
|
|
|
201
211
|
const pids = new Int32Array(capacity + 64);
|
|
202
212
|
const count = proc.symbols.proc_listallpids(ptr(pids), pids.byteLength);
|
|
203
213
|
if (count <= 0) return false;
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
{ uniqueId: bigint; parentUniqueId: bigint; processRef: NativeProcess }
|
|
207
|
-
>();
|
|
214
|
+
// First pass: collect identities for all pids, avoiding Process.fromPid calls.
|
|
215
|
+
const identities = new Map<number, { uniqueId: bigint; parentUniqueId: bigint }>();
|
|
208
216
|
for (let index = 0; index < count; index++) {
|
|
209
217
|
const pid = pids[index]!;
|
|
210
|
-
const
|
|
211
|
-
if (
|
|
218
|
+
const identity = uniqueIdentity(pid);
|
|
219
|
+
if (identity) identities.set(pid, identity);
|
|
220
|
+
}
|
|
221
|
+
// Extend ancestry evidence, then validate every listed pid that is known but
|
|
222
|
+
// not yet retained: new descendants plus earlier ones whose Process.fromPid
|
|
223
|
+
// check failed (it can return null for a live pid it could not query). A pid
|
|
224
|
+
// whose identity changed between the listing and the check was reused; skip it.
|
|
225
|
+
extendOwnedDarwinAncestry(knownUniqueIds, identities);
|
|
226
|
+
for (const [pid, listed] of identities) {
|
|
227
|
+
if (!knownUniqueIds.has(listed.uniqueId) || retainedUniqueIds.has(listed.uniqueId)) continue;
|
|
228
|
+
const before = Process.fromPid(pid);
|
|
229
|
+
if (!before) continue;
|
|
212
230
|
const identity = uniqueIdentity(pid);
|
|
213
231
|
const after = Process.fromPid(pid);
|
|
214
|
-
if (identity && after?.incarnation ===
|
|
215
|
-
|
|
232
|
+
if (identity?.uniqueId === listed.uniqueId && after?.incarnation === before.incarnation) {
|
|
233
|
+
retainOwnedProcess(owned, after);
|
|
234
|
+
retainedUniqueIds.add(listed.uniqueId);
|
|
216
235
|
}
|
|
217
236
|
}
|
|
218
|
-
for (const pid of extendOwnedDarwinAncestry(knownUniqueIds, candidates)) {
|
|
219
|
-
retainOwnedProcess(owned, candidates.get(pid)!.processRef);
|
|
220
|
-
}
|
|
221
237
|
return true;
|
|
222
238
|
},
|
|
223
239
|
close() {
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import { normalizePathForComparison, postmortem } from "@gajae-code/utils";
|
|
13
13
|
import { withFileLock } from "../config/file-lock";
|
|
14
14
|
import {
|
|
15
|
+
type CoordinatorAtomicWriteOptions,
|
|
15
16
|
ensureCoordinatorDirectory,
|
|
16
17
|
syncCoordinatorDirectory,
|
|
17
18
|
syncCoordinatorFile,
|
|
@@ -177,8 +178,25 @@ export const __sessionStateSidecarTestHooks: {
|
|
|
177
178
|
beforeRescopeJournalWrite?: (cwd: string) => void | Promise<void>;
|
|
178
179
|
beforePersistFromEvent?: (eventType: string, cwd: string) => void | Promise<void>;
|
|
179
180
|
beforeRescopePublish?: () => void | Promise<void>;
|
|
181
|
+
disableCoordinatorFileSyncForTests?: boolean;
|
|
180
182
|
} = {};
|
|
181
183
|
|
|
184
|
+
/** Create coordinator atomic write options with test hook support for disabling fsync. */
|
|
185
|
+
function coordinatorAtomicWriteOptionsForTests<T extends CoordinatorAtomicWriteOptions>(
|
|
186
|
+
overrides?: T,
|
|
187
|
+
): CoordinatorAtomicWriteOptions {
|
|
188
|
+
const options = overrides ?? ({} as T);
|
|
189
|
+
if (!__sessionStateSidecarTestHooks.disableCoordinatorFileSyncForTests) {
|
|
190
|
+
return options;
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
...options,
|
|
194
|
+
syncFile: async () => {
|
|
195
|
+
/* skip fsync in tests */
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
182
200
|
interface RuntimeStateEvent {
|
|
183
201
|
type: string;
|
|
184
202
|
messages?: unknown[];
|
|
@@ -1806,35 +1824,39 @@ async function writeStateFileSyncConditional(
|
|
|
1806
1824
|
expectedDestination: NativeExactFileIdentity | null,
|
|
1807
1825
|
): Promise<void> {
|
|
1808
1826
|
const contents = `${JSON.stringify(finalizeSidecarPayload(payload, keyId))}\n`;
|
|
1809
|
-
await writeCoordinatorAtomic(
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1827
|
+
await writeCoordinatorAtomic(
|
|
1828
|
+
stateFile,
|
|
1829
|
+
contents,
|
|
1830
|
+
coordinatorAtomicWriteOptionsForTests({
|
|
1831
|
+
rename: async (source: string, destination: string) => {
|
|
1832
|
+
if (!expectedDestination) {
|
|
1833
|
+
const published = renameNoReplacePath(source, destination);
|
|
1834
|
+
if (!published.ok) throw new PreviousRuntimeStateReadError();
|
|
1835
|
+
return;
|
|
1836
|
+
}
|
|
1837
|
+
const stat = fsSync.lstatSync(source, { bigint: true });
|
|
1838
|
+
const parent = fsSync.lstatSync(path.dirname(source), { bigint: true });
|
|
1839
|
+
if (!stat.isFile() || stat.isSymbolicLink() || !parent.isDirectory() || parent.isSymbolicLink())
|
|
1840
|
+
throw new PreviousRuntimeStateReadError();
|
|
1841
|
+
const replaced = exactReplacePath(
|
|
1842
|
+
source,
|
|
1843
|
+
destination,
|
|
1844
|
+
{
|
|
1845
|
+
dev: stat.dev,
|
|
1846
|
+
ino: stat.ino,
|
|
1847
|
+
nlink: stat.nlink,
|
|
1848
|
+
parentDev: parent.dev,
|
|
1849
|
+
parentIno: parent.ino,
|
|
1850
|
+
size: stat.size,
|
|
1851
|
+
mtimeNs: stat.mtimeNs,
|
|
1852
|
+
sha256: createHash("sha256").update(contents).digest("hex"),
|
|
1853
|
+
},
|
|
1854
|
+
expectedDestination,
|
|
1855
|
+
);
|
|
1856
|
+
if (!replaced.ok) throw new PreviousRuntimeStateReadError();
|
|
1857
|
+
},
|
|
1858
|
+
}),
|
|
1859
|
+
);
|
|
1838
1860
|
}
|
|
1839
1861
|
|
|
1840
1862
|
/**
|
|
@@ -1967,7 +1989,7 @@ async function withStateFileLocks<T>(stateFiles: readonly string[], operation: (
|
|
|
1967
1989
|
}
|
|
1968
1990
|
|
|
1969
1991
|
async function writeStateFile(stateFile: string, payload: Record<string, unknown>): Promise<void> {
|
|
1970
|
-
await writeCoordinatorAtomic(stateFile, `${JSON.stringify(payload)}\n
|
|
1992
|
+
await writeCoordinatorAtomic(stateFile, `${JSON.stringify(payload)}\n`, coordinatorAtomicWriteOptionsForTests());
|
|
1971
1993
|
}
|
|
1972
1994
|
|
|
1973
1995
|
function contextWithManagedOwnerGeneration(context: RuntimeStateContext): RuntimeStateContext {
|
|
@@ -2471,12 +2493,16 @@ export async function prepareCoordinatorRuntimeStateRescope(input: {
|
|
|
2471
2493
|
).toString("base64");
|
|
2472
2494
|
}
|
|
2473
2495
|
const writeJournal = async (journalFile: string): Promise<void> => {
|
|
2474
|
-
await writeCoordinatorAtomic(
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2496
|
+
await writeCoordinatorAtomic(
|
|
2497
|
+
journalFile,
|
|
2498
|
+
`${JSON.stringify(journal)}\n`,
|
|
2499
|
+
coordinatorAtomicWriteOptionsForTests({
|
|
2500
|
+
rename: async (source: string, destination: string) => {
|
|
2501
|
+
const published = renameNoReplacePath(source, destination);
|
|
2502
|
+
if (!published.ok) throw new PreviousRuntimeStateReadError();
|
|
2503
|
+
},
|
|
2504
|
+
}),
|
|
2505
|
+
);
|
|
2480
2506
|
};
|
|
2481
2507
|
await __sessionStateSidecarTestHooks.beforeRescopeJournalWrite?.(previousCwd);
|
|
2482
2508
|
assertPinnedDirectoryIdentity(input.previousCwd, previousCwdIdentity);
|
|
@@ -2532,28 +2558,32 @@ export async function markCoordinatorRuntimeStateRescopePublishing(
|
|
|
2532
2558
|
).toString("base64");
|
|
2533
2559
|
}
|
|
2534
2560
|
const contents = `${JSON.stringify(journal)}\n`;
|
|
2535
|
-
await writeCoordinatorAtomic(
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
source,
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2561
|
+
await writeCoordinatorAtomic(
|
|
2562
|
+
journalFile,
|
|
2563
|
+
contents,
|
|
2564
|
+
coordinatorAtomicWriteOptionsForTests({
|
|
2565
|
+
rename: async (source: string, destination: string) => {
|
|
2566
|
+
const sourceStat = fsSync.lstatSync(source, { bigint: true });
|
|
2567
|
+
const sourceParent = fsSync.lstatSync(path.dirname(source), { bigint: true });
|
|
2568
|
+
const replaced = exactReplacePath(
|
|
2569
|
+
source,
|
|
2570
|
+
destination,
|
|
2571
|
+
{
|
|
2572
|
+
dev: sourceStat.dev,
|
|
2573
|
+
ino: sourceStat.ino,
|
|
2574
|
+
nlink: sourceStat.nlink,
|
|
2575
|
+
parentDev: sourceParent.dev,
|
|
2576
|
+
parentIno: sourceParent.ino,
|
|
2577
|
+
size: sourceStat.size,
|
|
2578
|
+
mtimeNs: sourceStat.mtimeNs,
|
|
2579
|
+
sha256: createHash("sha256").update(contents).digest("hex"),
|
|
2580
|
+
},
|
|
2581
|
+
observed.identity,
|
|
2582
|
+
);
|
|
2583
|
+
if (!replaced.ok) throw new PreviousRuntimeStateReadError();
|
|
2584
|
+
},
|
|
2585
|
+
}),
|
|
2586
|
+
);
|
|
2557
2587
|
}
|
|
2558
2588
|
}
|
|
2559
2589
|
|