@gajae-code/coding-agent 0.18.1 → 0.18.5
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 +60 -0
- package/dist/types/cli/stats-cli.d.ts +0 -5
- package/dist/types/config/autorouting-tier-map.d.ts +29 -2
- package/dist/types/config/model-registry.d.ts +16 -1
- package/dist/types/exa/mcp-client.d.ts +1 -5
- package/dist/types/exec/bash-shell-guardian.d.ts +26 -0
- package/dist/types/hashline/parser.d.ts +28 -5
- package/dist/types/modes/acp/acp-agent.d.ts +6 -1
- package/dist/types/sdk/broker/lifecycle.d.ts +6 -1
- package/dist/types/sdk/broker/recovery-backoff.d.ts +62 -0
- package/dist/types/sdk/broker/runtime.d.ts +20 -0
- package/dist/types/sdk/broker/startup-budget.d.ts +5 -1
- package/dist/types/sdk/bus/index.d.ts +1 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
- package/dist/types/sdk/host/session-runtime.d.ts +7 -0
- package/dist/types/sdk/prompt-watchdog.d.ts +1 -1
- package/dist/types/tools/bash.d.ts +1 -0
- package/package.json +7 -7
- package/src/cli/stats-cli.ts +4 -36
- package/src/cli-main.ts +4 -8
- package/src/cli.ts +4 -8
- package/src/commands/commit.ts +2 -2
- package/src/commands/sdk.ts +1 -2
- package/src/config/autorouting-tier-map.ts +20 -2
- package/src/config/model-preset-registry.ts +17 -2
- package/src/config/model-profile-activation.ts +3 -1
- package/src/config/model-profiles.ts +11 -3
- package/src/config/model-registry.ts +45 -17
- package/src/exa/mcp-client.ts +41 -8
- package/src/exec/bash-shell-guardian.ts +184 -34
- package/src/hashline/execute.ts +51 -3
- package/src/hashline/parser.ts +63 -16
- package/src/internal-urls/docs-index.generated.ts +22 -22
- package/src/modes/acp/acp-agent.ts +39 -4
- package/src/modes/controllers/ssh-command-controller.ts +3 -1
- package/src/sdk/broker/lifecycle.ts +408 -218
- package/src/sdk/broker/recovery-backoff.ts +135 -0
- package/src/sdk/broker/runtime.ts +83 -0
- package/src/sdk/broker/startup-budget.ts +6 -1
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/index.ts +305 -19
- package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
- package/src/sdk/host/session-runtime.ts +99 -3
- package/src/sdk/prompt-watchdog.ts +17 -4
- package/src/session/agent-session.ts +14 -2
- package/src/session/internal/managed-session-storage.ts +23 -0
- package/src/tools/bash.ts +39 -0
- package/dist/types/cli/grep-cli.d.ts +0 -17
- package/dist/types/cli/shell-cli.d.ts +0 -8
- package/dist/types/commands/grep.d.ts +0 -42
- package/dist/types/commands/shell.d.ts +0 -21
- package/src/cli/grep-cli.ts +0 -160
- package/src/cli/shell-cli.ts +0 -176
- package/src/commands/grep.ts +0 -48
- package/src/commands/shell.ts +0 -29
|
@@ -45,8 +45,9 @@ export const TIER_MAP_VERSION = 1;
|
|
|
45
45
|
|
|
46
46
|
export const CURATED_TIER_LABELS = {
|
|
47
47
|
"anthropic/claude-haiku-4-5": [{ tier: "fast", rank: 1 }],
|
|
48
|
-
"anthropic/claude-sonnet-5": [{ tier: "balanced", rank: 1 }],
|
|
49
|
-
"anthropic/claude-sonnet-
|
|
48
|
+
"anthropic/claude-sonnet-5-5": [{ tier: "balanced", rank: 1 }],
|
|
49
|
+
"anthropic/claude-sonnet-5": [{ tier: "balanced", rank: 2 }],
|
|
50
|
+
"anthropic/claude-sonnet-4-6": [{ tier: "balanced", rank: 3 }],
|
|
50
51
|
"anthropic/claude-opus-5-5": [{ tier: "strong", effort: "high", rank: 1 }],
|
|
51
52
|
"anthropic/claude-opus-5": [{ tier: "strong", effort: "high", rank: 2 }],
|
|
52
53
|
"anthropic/claude-opus-4-8": [{ tier: "strong", effort: "high", rank: 3 }],
|
|
@@ -55,6 +56,7 @@ export const CURATED_TIER_LABELS = {
|
|
|
55
56
|
{ tier: "balanced", effort: "medium", rank: 1 },
|
|
56
57
|
],
|
|
57
58
|
"openai-codex/gpt-6-sol": [{ tier: "strong", effort: "high", rank: 1 }],
|
|
59
|
+
"openai-codex/gpt-6.1-sol": [{ tier: "strong", effort: "high", rank: 2 }],
|
|
58
60
|
"google/gemini-3.5-flash-lite": [{ tier: "fast", rank: 1 }],
|
|
59
61
|
"google/gemini-2.5-flash-lite": [{ tier: "fast", rank: 2 }],
|
|
60
62
|
"google/gemini-3.5-flash": [{ tier: "balanced", rank: 1 }],
|
|
@@ -75,6 +77,22 @@ export const CURATED_TIER_LABELS = {
|
|
|
75
77
|
|
|
76
78
|
/** Generated from packages/ai/src/models.json at feature land: 3936 in-scope baseline skips. */
|
|
77
79
|
export const TIER_MAP_SKIP_LIST = {
|
|
80
|
+
"amazon-bedrock/anthropic.claude-sonnet-5-5": { rationale: "Sonnet 5.5 catalog addition (#6111); not yet curated" },
|
|
81
|
+
"amazon-bedrock/au.anthropic.claude-sonnet-5-5": {
|
|
82
|
+
rationale: "Sonnet 5.5 catalog addition (#6111); not yet curated",
|
|
83
|
+
},
|
|
84
|
+
"amazon-bedrock/eu.anthropic.claude-sonnet-5-5": {
|
|
85
|
+
rationale: "Sonnet 5.5 catalog addition (#6111); not yet curated",
|
|
86
|
+
},
|
|
87
|
+
"amazon-bedrock/global.anthropic.claude-sonnet-5-5": {
|
|
88
|
+
rationale: "Sonnet 5.5 catalog addition (#6111); not yet curated",
|
|
89
|
+
},
|
|
90
|
+
"amazon-bedrock/jp.anthropic.claude-sonnet-5-5": {
|
|
91
|
+
rationale: "Sonnet 5.5 catalog addition (#6111); not yet curated",
|
|
92
|
+
},
|
|
93
|
+
"amazon-bedrock/us.anthropic.claude-sonnet-5-5": {
|
|
94
|
+
rationale: "Sonnet 5.5 catalog addition (#6111); not yet curated",
|
|
95
|
+
},
|
|
78
96
|
"kiro/claude-opus-5-5": { rationale: "post-rebase catalog addition from dev; not yet curated" },
|
|
79
97
|
"kiro/claude-opus-5.5": { rationale: "post-rebase catalog addition from dev; not yet curated" },
|
|
80
98
|
"alibaba-token-plan/qwen3.8-flash": { rationale: "post-rebase catalog addition from dev; not yet curated" },
|
|
@@ -652,6 +652,9 @@ function assertRegistryIdentityPolicy(value: unknown): void {
|
|
|
652
652
|
}
|
|
653
653
|
|
|
654
654
|
function assertCanonicalUnicode(value: string): void {
|
|
655
|
+
// Native well-formedness check first; the scan below only runs to name the
|
|
656
|
+
// offending surrogate when the string is malformed.
|
|
657
|
+
if (value.isWellFormed()) return;
|
|
655
658
|
for (let index = 0; index < value.length; index++) {
|
|
656
659
|
const code = value.charCodeAt(index);
|
|
657
660
|
if (code >= 0xd800 && code <= 0xdbff) {
|
|
@@ -665,6 +668,18 @@ function assertCanonicalUnicode(value: string): void {
|
|
|
665
668
|
}
|
|
666
669
|
}
|
|
667
670
|
|
|
671
|
+
/** Characters JSON.stringify escapes in strings (lone surrogates are rejected earlier). */
|
|
672
|
+
const JSON_STRING_ESCAPES = /["\\\u0000-\u001f]/;
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Quote a string exactly as JSON.stringify does. Registry documents are almost
|
|
676
|
+
* entirely plain ASCII identifiers, so skipping the per-call JSON.stringify for
|
|
677
|
+
* strings with nothing to escape removes most of the serializer's cost.
|
|
678
|
+
*/
|
|
679
|
+
function quoteCanonicalString(value: string): string {
|
|
680
|
+
return JSON_STRING_ESCAPES.test(value) ? JSON.stringify(value) : `"${value}"`;
|
|
681
|
+
}
|
|
682
|
+
|
|
668
683
|
function serializeCanonicalJson(value: unknown, stack: Set<object>): string {
|
|
669
684
|
if (value === null) return "null";
|
|
670
685
|
if (typeof value === "boolean") return value ? "true" : "false";
|
|
@@ -674,7 +689,7 @@ function serializeCanonicalJson(value: unknown, stack: Set<object>): string {
|
|
|
674
689
|
}
|
|
675
690
|
if (typeof value === "string") {
|
|
676
691
|
assertCanonicalUnicode(value);
|
|
677
|
-
return
|
|
692
|
+
return quoteCanonicalString(value);
|
|
678
693
|
}
|
|
679
694
|
if (typeof value !== "object") throw new TypeError(`Canonical JSON rejects ${typeof value}.`);
|
|
680
695
|
if (stack.has(value)) throw new TypeError("Canonical JSON rejects cycles.");
|
|
@@ -694,7 +709,7 @@ function serializeCanonicalJson(value: unknown, stack: Set<object>): string {
|
|
|
694
709
|
const entries: string[] = [];
|
|
695
710
|
for (const key of Object.keys(record).sort()) {
|
|
696
711
|
assertCanonicalUnicode(key);
|
|
697
|
-
entries.push(`${
|
|
712
|
+
entries.push(`${quoteCanonicalString(key)}:${serializeCanonicalJson(record[key], stack)}`);
|
|
698
713
|
}
|
|
699
714
|
return `{${entries.join(",")}}`;
|
|
700
715
|
} finally {
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
export { resolveModelProfileName } from "./model-profile-contract";
|
|
18
18
|
|
|
19
19
|
import {
|
|
20
|
+
createRegistrySelectorIndex,
|
|
20
21
|
GJC_MODEL_ASSIGNMENT_TARGETS,
|
|
21
22
|
type GjcModelAssignmentTargetId,
|
|
22
23
|
isAuthenticated,
|
|
@@ -675,6 +676,7 @@ export async function resolveMissingSessionModelRecovery(options: {
|
|
|
675
676
|
options.skips.every(skip => skip.reason === "unknown_model");
|
|
676
677
|
if (!allSelectorsUnknown) return undefined;
|
|
677
678
|
const fullCatalog = options.modelRegistry.getAll();
|
|
679
|
+
const catalogIndex = createRegistrySelectorIndex(fullCatalog);
|
|
678
680
|
const savedSelectorsMissingFromCatalog = resolveConfiguredModelPatterns(
|
|
679
681
|
options.defaultEntries,
|
|
680
682
|
options.settings,
|
|
@@ -688,7 +690,7 @@ export async function resolveMissingSessionModelRecovery(options: {
|
|
|
688
690
|
}).model
|
|
689
691
|
)
|
|
690
692
|
return false;
|
|
691
|
-
return !registrySelectorResolvesToModel(selector,
|
|
693
|
+
return !registrySelectorResolvesToModel(selector, catalogIndex);
|
|
692
694
|
});
|
|
693
695
|
const savedConcreteDefault = options.savedDefault ? parseModelString(options.savedDefault) : undefined;
|
|
694
696
|
const savedConcreteDefaultMissingFromCatalog =
|
|
@@ -93,6 +93,13 @@ export const BUILTIN_MODEL_PROFILES: readonly ModelProfileDefinition[] = [
|
|
|
93
93
|
critic: "openai-codex/gpt-6-sol:max",
|
|
94
94
|
architect: "openai-codex/gpt-6-sol:xhigh",
|
|
95
95
|
}),
|
|
96
|
+
profile("codex-sol61", ["openai-codex"], {
|
|
97
|
+
default: "openai-codex/gpt-6.1-sol:medium",
|
|
98
|
+
executor: "openai-codex/gpt-6.1-sol:medium",
|
|
99
|
+
planner: "openai-codex/gpt-6.1-sol:high",
|
|
100
|
+
critic: "openai-codex/gpt-6.1-sol:xhigh",
|
|
101
|
+
architect: "openai-codex/gpt-6-astra:xhigh",
|
|
102
|
+
}),
|
|
96
103
|
profile("lunamaxxing", ["openai-codex"], {
|
|
97
104
|
default: "openai-codex/gpt-6-luna:medium",
|
|
98
105
|
executor: "openai-codex/gpt-6-luna:xhigh",
|
|
@@ -256,14 +263,14 @@ export const BUILTIN_MODEL_PROFILES: readonly ModelProfileDefinition[] = [
|
|
|
256
263
|
}),
|
|
257
264
|
profile("claude-opus", ["anthropic"], {
|
|
258
265
|
default: ["anthropic/claude-opus-5-5:medium", "anthropic/claude-opus-4-6:xhigh"],
|
|
259
|
-
executor: "anthropic/claude-sonnet-5",
|
|
266
|
+
executor: "anthropic/claude-sonnet-5-5",
|
|
260
267
|
planner: ["anthropic/claude-opus-5-5:medium", "anthropic/claude-opus-4-6:low"],
|
|
261
268
|
critic: ["anthropic/claude-opus-5-5:high", "anthropic/claude-opus-4-6:high"],
|
|
262
269
|
architect: ["anthropic/claude-opus-5-5:high", "anthropic/claude-opus-4-6:xhigh"],
|
|
263
270
|
}),
|
|
264
271
|
profile("claude-fable", ["anthropic"], {
|
|
265
272
|
default: "anthropic/claude-fable-5-1:xhigh",
|
|
266
|
-
executor: "anthropic/claude-sonnet-5",
|
|
273
|
+
executor: "anthropic/claude-sonnet-5-5",
|
|
267
274
|
planner: "anthropic/claude-fable-5-1:low",
|
|
268
275
|
critic: "anthropic/claude-fable-5-1:high",
|
|
269
276
|
architect: "anthropic/claude-fable-5-1:xhigh",
|
|
@@ -491,7 +498,7 @@ export const BUILTIN_MODEL_PROFILES: readonly ModelProfileDefinition[] = [
|
|
|
491
498
|
profile("opus-codex", ["anthropic", "openai-codex"], {
|
|
492
499
|
default: "anthropic/claude-opus-5-5:medium",
|
|
493
500
|
executor: "openai-codex/gpt-5.6-terra:low",
|
|
494
|
-
planner: "anthropic/claude-sonnet-5",
|
|
501
|
+
planner: "anthropic/claude-sonnet-5-5",
|
|
495
502
|
critic: "openai-codex/gpt-6-sol:xhigh",
|
|
496
503
|
architect: "openai-codex/gpt-6-sol:high",
|
|
497
504
|
}),
|
|
@@ -555,6 +562,7 @@ const PROFILE_PRESENTATION: Record<string, ModelProfilePresentation> = {
|
|
|
555
562
|
"codex-eco": { displayName: "Codex Eco", providerGroup: "CODEX" },
|
|
556
563
|
"codex-medium": { displayName: "Codex Medium", providerGroup: "CODEX" },
|
|
557
564
|
"codex-pro": { displayName: "Codex Pro", providerGroup: "CODEX" },
|
|
565
|
+
"codex-sol61": { displayName: "Codex Sol 6.1", providerGroup: "CODEX" },
|
|
558
566
|
lunamaxxing: { displayName: "LunaMaxxing", providerGroup: "CODEX" },
|
|
559
567
|
"astra-lite": { displayName: "ASTRA-Lite", providerGroup: "CODEX" },
|
|
560
568
|
"astra-default": { displayName: "ASTRA-Default", providerGroup: "CODEX" },
|
|
@@ -782,26 +782,53 @@ function registryModelMetadataWithoutApiSpecificFields(model: Model<Api>): Parti
|
|
|
782
782
|
return metadata;
|
|
783
783
|
}
|
|
784
784
|
|
|
785
|
-
|
|
785
|
+
/**
|
|
786
|
+
* Lowercased lookup keys for a model catalog, so selector resolution is a set
|
|
787
|
+
* lookup instead of a scan that re-lowercases every model id per selector.
|
|
788
|
+
*/
|
|
789
|
+
export interface RegistrySelectorIndex {
|
|
790
|
+
/** `id` and `provider/id`. */
|
|
791
|
+
exact: ReadonlySet<string>;
|
|
792
|
+
/** `id` alone. */
|
|
793
|
+
ids: ReadonlySet<string>;
|
|
794
|
+
/** Model ids per provider, for parsed `provider/id` selectors (nested, so no delimiter can collide). */
|
|
795
|
+
providerIds: ReadonlyMap<string, ReadonlySet<string>>;
|
|
796
|
+
/** Every `/`-delimited suffix of each id (`a/b/c` → `b/c`, `c`). */
|
|
797
|
+
idSuffixes: ReadonlySet<string>;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
export function createRegistrySelectorIndex(models: readonly Model<Api>[]): RegistrySelectorIndex {
|
|
801
|
+
const exact = new Set<string>();
|
|
802
|
+
const ids = new Set<string>();
|
|
803
|
+
const providerIds = new Map<string, Set<string>>();
|
|
804
|
+
const idSuffixes = new Set<string>();
|
|
805
|
+
for (const model of models) {
|
|
806
|
+
const id = model.id.toLowerCase();
|
|
807
|
+
const provider = model.provider.toLowerCase();
|
|
808
|
+
ids.add(id);
|
|
809
|
+
exact.add(id);
|
|
810
|
+
exact.add(`${provider}/${id}`);
|
|
811
|
+
let providerModelIds = providerIds.get(provider);
|
|
812
|
+
if (!providerModelIds) {
|
|
813
|
+
providerModelIds = new Set<string>();
|
|
814
|
+
providerIds.set(provider, providerModelIds);
|
|
815
|
+
}
|
|
816
|
+
providerModelIds.add(id);
|
|
817
|
+
for (let slash = id.indexOf("/"); slash !== -1; slash = id.indexOf("/", slash + 1)) {
|
|
818
|
+
idSuffixes.add(id.slice(slash + 1));
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
return { exact, ids, providerIds, idSuffixes };
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
export function registrySelectorResolvesToModel(selector: string, index: RegistrySelectorIndex): boolean {
|
|
786
825
|
const normalizedSelector = selector.trim().toLowerCase();
|
|
787
|
-
if (
|
|
788
|
-
models.some(
|
|
789
|
-
model =>
|
|
790
|
-
model.id.toLowerCase() === normalizedSelector ||
|
|
791
|
-
`${model.provider}/${model.id}`.toLowerCase() === normalizedSelector,
|
|
792
|
-
)
|
|
793
|
-
)
|
|
794
|
-
return true;
|
|
826
|
+
if (index.exact.has(normalizedSelector)) return true;
|
|
795
827
|
const suffix = splitSelectorThinkingSuffix(normalizedSelector);
|
|
796
828
|
const baseSelector = suffix.thinkingLevel === undefined ? normalizedSelector : suffix.selector;
|
|
797
829
|
const parsed = parseModelString(baseSelector);
|
|
798
|
-
if (parsed)
|
|
799
|
-
|
|
800
|
-
model => model.provider.toLowerCase() === parsed.provider && model.id.toLowerCase() === parsed.id,
|
|
801
|
-
);
|
|
802
|
-
return models.some(
|
|
803
|
-
model => model.id.toLowerCase() === baseSelector || model.id.toLowerCase().endsWith(`/${baseSelector}`),
|
|
804
|
-
);
|
|
830
|
+
if (parsed) return index.providerIds.get(parsed.provider)?.has(parsed.id) ?? false;
|
|
831
|
+
return index.ids.has(baseSelector) || index.idSuffixes.has(baseSelector);
|
|
805
832
|
}
|
|
806
833
|
|
|
807
834
|
function filterMaterializedRegistryProfiles(
|
|
@@ -810,6 +837,7 @@ function filterMaterializedRegistryProfiles(
|
|
|
810
837
|
dynamicProviders: ReadonlySet<string>,
|
|
811
838
|
): Map<string, ModelProfileDefinition> {
|
|
812
839
|
const filtered = new Map<string, ModelProfileDefinition>();
|
|
840
|
+
const index = createRegistrySelectorIndex(models);
|
|
813
841
|
for (const [name, profile] of profiles) {
|
|
814
842
|
if (
|
|
815
843
|
profile.source === "registry" &&
|
|
@@ -818,7 +846,7 @@ function filterMaterializedRegistryProfiles(
|
|
|
818
846
|
return (
|
|
819
847
|
selectors.length > 0 &&
|
|
820
848
|
!selectors.some(selector => {
|
|
821
|
-
if (registrySelectorResolvesToModel(selector,
|
|
849
|
+
if (registrySelectorResolvesToModel(selector, index)) return true;
|
|
822
850
|
const suffix = splitSelectorThinkingSuffix(selector);
|
|
823
851
|
const parsed = parseModelString(suffix.thinkingLevel ? suffix.selector : selector);
|
|
824
852
|
return parsed !== undefined && dynamicProviders.has(parsed.provider);
|
package/src/exa/mcp-client.ts
CHANGED
|
@@ -187,7 +187,16 @@ export function formatSearchResults(data: ExaSearchResponse): string {
|
|
|
187
187
|
* Format a non-search MCP response as human-readable text.
|
|
188
188
|
* Handles objects, arrays, primitives, and common MCP response shapes.
|
|
189
189
|
*/
|
|
190
|
-
|
|
190
|
+
// Nesting depth is bounded. Each object level re-indents the entire formatted
|
|
191
|
+
// subtree, so cost grows super-linearly with depth: 500/1000/2000/4000/8000
|
|
192
|
+
// levels cost 60ms/227ms/730ms/5.9s/94s. A tool-call payload is whatever the
|
|
193
|
+
// remote MCP server returned, and `MCP_MAX_CONTENT_BYTES` does not bound depth
|
|
194
|
+
// — 8000 levels of `{"a":` is under 47 KB. The limit matches the one the
|
|
195
|
+
// session-import walker already applies (`sanitizeImportedValue`, depth > 64).
|
|
196
|
+
const MAX_GENERIC_RESPONSE_DEPTH = 64;
|
|
197
|
+
|
|
198
|
+
export function formatGenericResponse(data: unknown, depth = 0): string {
|
|
199
|
+
if (depth > MAX_GENERIC_RESPONSE_DEPTH) return "[depth-limit]";
|
|
191
200
|
if (data === null || data === undefined) return "No result.";
|
|
192
201
|
if (typeof data === "string") return data;
|
|
193
202
|
if (typeof data === "number" || typeof data === "boolean") return String(data);
|
|
@@ -199,14 +208,22 @@ export function formatGenericResponse(data: unknown): string {
|
|
|
199
208
|
const item = data[i];
|
|
200
209
|
if (typeof item === "object" && item !== null) {
|
|
201
210
|
const record = item as Record<string, unknown>;
|
|
202
|
-
const
|
|
211
|
+
const rawTitle = record.title ?? record.name ?? record.id;
|
|
212
|
+
// A remote title is unvalidated JSON: stringify non-strings through the
|
|
213
|
+
// depth-bounded path instead of template coercion, which recurses unbounded.
|
|
214
|
+
const title =
|
|
215
|
+
rawTitle === undefined || rawTitle === null
|
|
216
|
+
? `Item ${i + 1}`
|
|
217
|
+
: typeof rawTitle === "string"
|
|
218
|
+
? rawTitle
|
|
219
|
+
: formatValue(rawTitle, depth + 2);
|
|
203
220
|
parts.push(`\n### ${title}`);
|
|
204
221
|
for (const [k, v] of Object.entries(record)) {
|
|
205
222
|
if (["title", "name", "id"].includes(k)) continue;
|
|
206
|
-
parts.push(`- **${k}:** ${formatValue(v)}`);
|
|
223
|
+
parts.push(`- **${k}:** ${formatValue(v, depth + 2)}`);
|
|
207
224
|
}
|
|
208
225
|
} else {
|
|
209
|
-
parts.push(`- ${formatValue(item)}`);
|
|
226
|
+
parts.push(`- ${formatValue(item, depth + 1)}`);
|
|
210
227
|
}
|
|
211
228
|
}
|
|
212
229
|
return parts.join("\n");
|
|
@@ -231,10 +248,10 @@ export function formatGenericResponse(data: unknown): string {
|
|
|
231
248
|
if (k === "content") continue; // handled above
|
|
232
249
|
if (v === null || v === undefined) continue;
|
|
233
250
|
if (typeof v === "object") {
|
|
234
|
-
const formatted = formatGenericResponse(v);
|
|
251
|
+
const formatted = formatGenericResponse(v, depth + 1);
|
|
235
252
|
if (formatted) lines.push(`- **${k}:**\n${indent(formatted, 2)}`);
|
|
236
253
|
} else {
|
|
237
|
-
lines.push(`- **${k}:** ${formatValue(v)}`);
|
|
254
|
+
lines.push(`- **${k}:** ${formatValue(v, depth + 1)}`);
|
|
238
255
|
}
|
|
239
256
|
}
|
|
240
257
|
return lines.join("\n") || "(empty)";
|
|
@@ -243,12 +260,28 @@ export function formatGenericResponse(data: unknown): string {
|
|
|
243
260
|
return String(data);
|
|
244
261
|
}
|
|
245
262
|
|
|
246
|
-
function formatValue(v: unknown): string {
|
|
263
|
+
function formatValue(v: unknown, depth: number): string {
|
|
247
264
|
if (v === null || v === undefined) return "—";
|
|
248
|
-
if (typeof v === "object") return JSON.stringify(v);
|
|
265
|
+
if (typeof v === "object") return JSON.stringify(truncateDepth(v, depth));
|
|
249
266
|
return String(v);
|
|
250
267
|
}
|
|
251
268
|
|
|
269
|
+
/**
|
|
270
|
+
* Copies a value, replacing anything nested past MAX_GENERIC_RESPONSE_DEPTH with
|
|
271
|
+
* "[depth-limit]", so JSON.stringify never recurses through a remote payload's
|
|
272
|
+
* unbounded structure.
|
|
273
|
+
*/
|
|
274
|
+
function truncateDepth(value: unknown, depth: number): unknown {
|
|
275
|
+
if (value === null || typeof value !== "object") return value;
|
|
276
|
+
if (depth > MAX_GENERIC_RESPONSE_DEPTH) return "[depth-limit]";
|
|
277
|
+
if (Array.isArray(value)) return value.map(item => truncateDepth(item, depth + 1));
|
|
278
|
+
// Null prototype: an own "__proto__" key from a parsed payload must stay an
|
|
279
|
+
// own data property instead of hitting the Object.prototype setter.
|
|
280
|
+
const copy: Record<string, unknown> = Object.create(null);
|
|
281
|
+
for (const [key, child] of Object.entries(value)) copy[key] = truncateDepth(child, depth + 1);
|
|
282
|
+
return copy;
|
|
283
|
+
}
|
|
284
|
+
|
|
252
285
|
function indent(text: string, spaces: number): string {
|
|
253
286
|
const pad = " ".repeat(spaces);
|
|
254
287
|
return text
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { dlopen, FFIType, ptr } from "bun:ffi";
|
|
2
2
|
import * as childProcess from "node:child_process";
|
|
3
3
|
import { createHmac, randomUUID, timingSafeEqual } from "node:crypto";
|
|
4
|
+
import * as fsSync from "node:fs";
|
|
4
5
|
import * as fs from "node:fs/promises";
|
|
5
6
|
import * as os from "node:os";
|
|
6
7
|
import * as path from "node:path";
|
|
@@ -63,10 +64,10 @@ export function extendOwnedDarwinAncestry<T extends { uniqueId: bigint; parentUn
|
|
|
63
64
|
return addedPids;
|
|
64
65
|
}
|
|
65
66
|
|
|
66
|
-
function authenticateOwnershipRecord(
|
|
67
|
+
export function authenticateOwnershipRecord(
|
|
67
68
|
line: string,
|
|
68
69
|
ledgerToken: string,
|
|
69
|
-
): { processRef?: NativeProcess; darwinUniqueId?: bigint } | undefined {
|
|
70
|
+
): { processRef?: NativeProcess; darwinUniqueId?: bigint; pending?: true } | undefined {
|
|
70
71
|
const record = parseOwnershipRecord(line);
|
|
71
72
|
if (!record) return undefined;
|
|
72
73
|
const uniqueId = record.darwinUniqueId ?? "";
|
|
@@ -80,15 +81,20 @@ function authenticateOwnershipRecord(
|
|
|
80
81
|
return undefined;
|
|
81
82
|
}
|
|
82
83
|
if (received.byteLength !== expected.byteLength || !timingSafeEqual(received, expected)) return undefined;
|
|
83
|
-
if (process.platform === "darwin" && !record.darwinUniqueId) return undefined;
|
|
84
84
|
const { Process } = require("@gajae-code/natives") as NativeProcessBindings;
|
|
85
85
|
const owned = Process.fromPid(record.pid);
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
86
|
+
const darwinUniqueId = record.darwinUniqueId ? { darwinUniqueId: BigInt(record.darwinUniqueId) } : {};
|
|
87
|
+
if (owned?.incarnation === record.incarnation) return { processRef: owned, ...darwinUniqueId };
|
|
88
|
+
// fromPid() conflates death with an inconclusive lookup. Only a confirmed
|
|
89
|
+
// absence or a different incarnation settles the record; an unknown result
|
|
90
|
+
// keeps it pending so the caller retries the line on the next scan.
|
|
91
|
+
if (!owned && Process.observe(record.pid).status === "unknown") return { pending: true, ...darwinUniqueId };
|
|
92
|
+
return darwinUniqueId;
|
|
90
93
|
}
|
|
91
94
|
|
|
95
|
+
/** Scans an inconclusive ledger record may stay pending before tracking fails closed. */
|
|
96
|
+
const MAX_PENDING_LEDGER_ATTEMPTS = 50;
|
|
97
|
+
|
|
92
98
|
function enableLinuxChildSubreaper(): boolean {
|
|
93
99
|
if (process.platform !== "linux") return true;
|
|
94
100
|
try {
|
|
@@ -157,14 +163,27 @@ function findDarwinLedgerHolders(device: number, inode: bigint): NativeProcess[]
|
|
|
157
163
|
}
|
|
158
164
|
}
|
|
159
165
|
|
|
160
|
-
type DarwinAncestryTracker = {
|
|
166
|
+
export type DarwinAncestryTracker = {
|
|
161
167
|
seed(uniqueId: bigint): void;
|
|
162
168
|
track(processRef: NativeProcess, uniqueId?: bigint): boolean;
|
|
169
|
+
trackGuardian(processRef: NativeProcess, uniqueId?: bigint): boolean;
|
|
163
170
|
poll(): boolean;
|
|
164
171
|
close(): void;
|
|
165
172
|
};
|
|
166
173
|
|
|
167
|
-
|
|
174
|
+
export type DarwinIdentity = { uniqueId: bigint; parentUniqueId: bigint };
|
|
175
|
+
|
|
176
|
+
/** Test seams for the Darwin tracker; production uses libproc and the natives binding. */
|
|
177
|
+
export type DarwinAncestryTrackerDeps = {
|
|
178
|
+
uniqueIdentity?: (pid: number) => DarwinIdentity | undefined;
|
|
179
|
+
fromPid?: (pid: number) => NativeProcess | null | undefined;
|
|
180
|
+
observe?: (pid: number) => { status: "present" | "absent" | "unknown"; incarnation?: string };
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export function createDarwinAncestryTracker(
|
|
184
|
+
owned: Map<string, NativeProcess>,
|
|
185
|
+
deps: DarwinAncestryTrackerDeps = {},
|
|
186
|
+
): DarwinAncestryTracker | undefined {
|
|
168
187
|
if (process.platform !== "darwin") return undefined;
|
|
169
188
|
try {
|
|
170
189
|
const proc = dlopen("/usr/lib/libproc.dylib", {
|
|
@@ -180,10 +199,12 @@ function createDarwinAncestryTracker(owned: Map<string, NativeProcess>): DarwinA
|
|
|
180
199
|
// own children still chain, but it is retried on every poll until retained.
|
|
181
200
|
const retainedUniqueIds = new Set<bigint>();
|
|
182
201
|
const { Process } = require("@gajae-code/natives") as NativeProcessBindings;
|
|
202
|
+
const fromPid = deps.fromPid ?? ((pid: number) => Process.fromPid(pid));
|
|
203
|
+
const observe = deps.observe ?? ((pid: number) => Process.observe(pid));
|
|
183
204
|
// Reuse buffer across poll iterations to avoid allocations inside the loop.
|
|
184
205
|
const identityBuffer = new Uint8Array(56);
|
|
185
206
|
const identityView = new DataView(identityBuffer.buffer);
|
|
186
|
-
const
|
|
207
|
+
const defaultUniqueIdentity = (pid: number): { uniqueId: bigint; parentUniqueId: bigint } | undefined => {
|
|
187
208
|
const bytes = proc.symbols.proc_pidinfo(pid, 17, 0, ptr(identityBuffer), identityBuffer.byteLength);
|
|
188
209
|
if (bytes !== identityBuffer.byteLength) return undefined;
|
|
189
210
|
return {
|
|
@@ -191,9 +212,92 @@ function createDarwinAncestryTracker(owned: Map<string, NativeProcess>): DarwinA
|
|
|
191
212
|
parentUniqueId: identityView.getBigUint64(24, true),
|
|
192
213
|
};
|
|
193
214
|
};
|
|
215
|
+
const uniqueIdentity = deps.uniqueIdentity ?? defaultUniqueIdentity;
|
|
216
|
+
const incarnationOnlyAnchors: NativeProcess[] = [];
|
|
217
|
+
const anchorKeys = new Set<string>();
|
|
218
|
+
const addIncarnationOnlyAnchor = (processRef: NativeProcess): void => {
|
|
219
|
+
const key = `${processRef.pid}:${processRef.incarnation}`;
|
|
220
|
+
if (anchorKeys.has(key)) return;
|
|
221
|
+
anchorKeys.add(key);
|
|
222
|
+
incarnationOnlyAnchors.push(processRef);
|
|
223
|
+
};
|
|
224
|
+
// children() validates its parent and then snapshots the process table; the
|
|
225
|
+
// parent can exit and its pid be reused in between, returning a stranger's
|
|
226
|
+
// children. Only trust a snapshot if the parent still holds its incarnation
|
|
227
|
+
// after it was taken.
|
|
228
|
+
const childrenOf = (parent: NativeProcess): NativeProcess[] => {
|
|
229
|
+
const children = parent.children();
|
|
230
|
+
return fromPid(parent.pid)?.incarnation === parent.incarnation ? children : [];
|
|
231
|
+
};
|
|
232
|
+
const seedAnchorDescendants = (anchor: NativeProcess): void => {
|
|
233
|
+
const pending = [...childrenOf(anchor)];
|
|
234
|
+
while (pending.length > 0) {
|
|
235
|
+
const child = pending.pop()!;
|
|
236
|
+
retainOwnedProcess(owned, child);
|
|
237
|
+
// The unique id is read from a bare pid, so bracket it with incarnation
|
|
238
|
+
// checks against this handle: a pid reused in between would otherwise
|
|
239
|
+
// seed an unrelated process (and its descendants) as owned.
|
|
240
|
+
if (fromPid(child.pid)?.incarnation === child.incarnation) {
|
|
241
|
+
const childId = uniqueIdentity(child.pid)?.uniqueId;
|
|
242
|
+
if (fromPid(child.pid)?.incarnation === child.incarnation) {
|
|
243
|
+
if (childId !== undefined) {
|
|
244
|
+
knownUniqueIds.add(childId);
|
|
245
|
+
retainedUniqueIds.add(childId);
|
|
246
|
+
} else {
|
|
247
|
+
// Its flavor-17 query is denied too: without a unique id the
|
|
248
|
+
// ancestry walk can never reach its subtree, so it must outlive
|
|
249
|
+
// this anchor's pruning as an incarnation-only anchor itself.
|
|
250
|
+
addIncarnationOnlyAnchor(child);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
pending.push(...childrenOf(child));
|
|
255
|
+
}
|
|
256
|
+
};
|
|
194
257
|
const track = (processRef: NativeProcess, signedUniqueId?: bigint): boolean => {
|
|
258
|
+
// The bare-pid unique-id query is only trusted if the same incarnation
|
|
259
|
+
// holds on both sides of it; otherwise a pid reused after the record was
|
|
260
|
+
// authenticated would seed the replacement's ancestry as owned.
|
|
261
|
+
let identity: { uniqueId: bigint; parentUniqueId: bigint } | undefined;
|
|
262
|
+
if (signedUniqueId === undefined) {
|
|
263
|
+
const queried = uniqueIdentity(processRef.pid);
|
|
264
|
+
const after = observe(processRef.pid);
|
|
265
|
+
if (
|
|
266
|
+
after.status === "absent" ||
|
|
267
|
+
(after.status === "present" && after.incarnation !== processRef.incarnation)
|
|
268
|
+
) {
|
|
269
|
+
// Confirmed exited or reused since authentication: retain nothing new.
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
// Present with the same incarnation keeps the queried id. An
|
|
273
|
+
// inconclusive lookup cannot vouch for it, so the record falls through
|
|
274
|
+
// as incarnation-only and is kept as an anchor rather than dropped.
|
|
275
|
+
identity = after.status === "present" ? queried : undefined;
|
|
276
|
+
}
|
|
277
|
+
const uniqueId = signedUniqueId ?? identity?.uniqueId;
|
|
278
|
+
if (uniqueId === undefined) {
|
|
279
|
+
// Incarnation-only record: the child has no unique id to anchor the
|
|
280
|
+
// ancestry graph. Keep it as an anchor whose live descendants poll()
|
|
281
|
+
// seeds by unique id, so their subtrees stay tracked after the child
|
|
282
|
+
// exits and they reparent (PPID discovery alone would lose them).
|
|
283
|
+
// Residual gap (accepted over #6085's exit 70): if the child forks a
|
|
284
|
+
// descendant that leaves the supervisor process group, then exits before
|
|
285
|
+
// this record is scanned, nothing links that descendant back to us. The
|
|
286
|
+
// ledger watcher scans on append to keep that window minimal.
|
|
287
|
+
retainOwnedProcess(owned, processRef);
|
|
288
|
+
addIncarnationOnlyAnchor(processRef);
|
|
289
|
+
seedAnchorDescendants(processRef);
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
knownUniqueIds.add(uniqueId);
|
|
293
|
+
retainedUniqueIds.add(uniqueId);
|
|
294
|
+
retainOwnedProcess(owned, processRef);
|
|
295
|
+
return true;
|
|
296
|
+
};
|
|
297
|
+
const trackGuardian = (processRef: NativeProcess, signedUniqueId?: bigint): boolean => {
|
|
195
298
|
const identity = signedUniqueId === undefined ? uniqueIdentity(processRef.pid) : undefined;
|
|
196
299
|
const uniqueId = signedUniqueId ?? identity?.uniqueId;
|
|
300
|
+
// Guardian registration is strict: must have a unique id
|
|
197
301
|
if (uniqueId === undefined) return false;
|
|
198
302
|
knownUniqueIds.add(uniqueId);
|
|
199
303
|
retainedUniqueIds.add(uniqueId);
|
|
@@ -205,7 +309,19 @@ function createDarwinAncestryTracker(owned: Map<string, NativeProcess>): DarwinA
|
|
|
205
309
|
knownUniqueIds.add(uniqueId);
|
|
206
310
|
},
|
|
207
311
|
track,
|
|
312
|
+
trackGuardian,
|
|
208
313
|
poll() {
|
|
314
|
+
// Re-seed live anchors, then drop exited ones: their descendants were
|
|
315
|
+
// seeded by unique id while the anchor was alive, and ancestry now
|
|
316
|
+
// carries them, so re-walking a dead anchor only costs time.
|
|
317
|
+
for (let index = incarnationOnlyAnchors.length - 1; index >= 0; index--) {
|
|
318
|
+
const anchor = incarnationOnlyAnchors[index]!;
|
|
319
|
+
seedAnchorDescendants(anchor);
|
|
320
|
+
if (fromPid(anchor.pid)?.incarnation !== anchor.incarnation) {
|
|
321
|
+
incarnationOnlyAnchors.splice(index, 1);
|
|
322
|
+
anchorKeys.delete(`${anchor.pid}:${anchor.incarnation}`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
209
325
|
const capacity = proc.symbols.proc_listallpids(null, 0);
|
|
210
326
|
if (capacity <= 0) return false;
|
|
211
327
|
const pids = new Int32Array(capacity + 64);
|
|
@@ -225,10 +341,10 @@ function createDarwinAncestryTracker(owned: Map<string, NativeProcess>): DarwinA
|
|
|
225
341
|
extendOwnedDarwinAncestry(knownUniqueIds, identities);
|
|
226
342
|
for (const [pid, listed] of identities) {
|
|
227
343
|
if (!knownUniqueIds.has(listed.uniqueId) || retainedUniqueIds.has(listed.uniqueId)) continue;
|
|
228
|
-
const before =
|
|
344
|
+
const before = fromPid(pid);
|
|
229
345
|
if (!before) continue;
|
|
230
346
|
const identity = uniqueIdentity(pid);
|
|
231
|
-
const after =
|
|
347
|
+
const after = fromPid(pid);
|
|
232
348
|
if (identity?.uniqueId === listed.uniqueId && after?.incarnation === before.incarnation) {
|
|
233
349
|
retainOwnedProcess(owned, after);
|
|
234
350
|
retainedUniqueIds.add(listed.uniqueId);
|
|
@@ -275,7 +391,7 @@ export async function runBashShellGuardian(): Promise<void> {
|
|
|
275
391
|
if (darwinTracker) {
|
|
276
392
|
const { Process } = require("@gajae-code/natives") as NativeProcessBindings;
|
|
277
393
|
const guardian = Process.fromPid(process.pid);
|
|
278
|
-
if (!guardian || !darwinTracker.
|
|
394
|
+
if (!guardian || !darwinTracker.trackGuardian(guardian)) {
|
|
279
395
|
darwinTracker.close();
|
|
280
396
|
await ledger.close();
|
|
281
397
|
await fs.rm(ownershipFilePath, { force: true });
|
|
@@ -306,6 +422,8 @@ export async function runBashShellGuardian(): Promise<void> {
|
|
|
306
422
|
input.once("close", () => supervisor.stdin.end());
|
|
307
423
|
let cleaning: Promise<void> | undefined;
|
|
308
424
|
let ledgerBuffer = "";
|
|
425
|
+
let pendingLedgerLines: string[] = [];
|
|
426
|
+
const pendingAttempts = new Map<string, number>();
|
|
309
427
|
let ownershipScan = Promise.resolve(true);
|
|
310
428
|
const scanOwnership = (): Promise<boolean> => {
|
|
311
429
|
ownershipScan = ownershipScan.then(async previousOk => {
|
|
@@ -317,11 +435,23 @@ export async function runBashShellGuardian(): Promise<void> {
|
|
|
317
435
|
return false;
|
|
318
436
|
}
|
|
319
437
|
ledgerBuffer += content;
|
|
320
|
-
const
|
|
321
|
-
ledgerBuffer =
|
|
438
|
+
const fresh = ledgerBuffer.split("\n");
|
|
439
|
+
ledgerBuffer = fresh.pop() ?? "";
|
|
440
|
+
const lines = [...pendingLedgerLines, ...fresh];
|
|
441
|
+
pendingLedgerLines = [];
|
|
322
442
|
for (const line of lines) {
|
|
323
443
|
if (!line) continue;
|
|
324
444
|
const owned = authenticateOwnershipRecord(line, ledgerToken);
|
|
445
|
+
if (owned?.pending) {
|
|
446
|
+
// Inconclusive lookup: retry next scan, bounded so a pid stuck in an
|
|
447
|
+
// unknown state cannot grow the queue or starve cleanup forever.
|
|
448
|
+
const attempts = (pendingAttempts.get(line) ?? 0) + 1;
|
|
449
|
+
if (attempts > MAX_PENDING_LEDGER_ATTEMPTS) return false;
|
|
450
|
+
pendingAttempts.set(line, attempts);
|
|
451
|
+
pendingLedgerLines.push(line);
|
|
452
|
+
} else {
|
|
453
|
+
pendingAttempts.delete(line);
|
|
454
|
+
}
|
|
325
455
|
if (owned?.darwinUniqueId && darwinTracker) darwinTracker.seed(owned.darwinUniqueId);
|
|
326
456
|
if (owned?.processRef && darwinTracker && !darwinTracker.track(owned.processRef, owned.darwinUniqueId))
|
|
327
457
|
return false;
|
|
@@ -335,28 +465,48 @@ export async function runBashShellGuardian(): Promise<void> {
|
|
|
335
465
|
return ownershipScan;
|
|
336
466
|
};
|
|
337
467
|
let periodicScanActive = false;
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
468
|
+
let periodicScanRequested = false;
|
|
469
|
+
const runPeriodicScan = (): void => {
|
|
470
|
+
if (periodicScanActive) {
|
|
471
|
+
periodicScanRequested = true;
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
periodicScanActive = true;
|
|
475
|
+
void scanOwnership()
|
|
476
|
+
.then(ok => {
|
|
477
|
+
if (ok || supervisor.exitCode !== null || supervisor.signalCode !== null) return;
|
|
478
|
+
if (supervisor.pid) {
|
|
479
|
+
try {
|
|
480
|
+
process.kill(-supervisor.pid, "SIGKILL");
|
|
481
|
+
} catch {}
|
|
482
|
+
}
|
|
483
|
+
supervisor.kill("SIGKILL");
|
|
484
|
+
})
|
|
485
|
+
.finally(() => {
|
|
486
|
+
periodicScanActive = false;
|
|
487
|
+
if (periodicScanRequested && !cleaning) {
|
|
488
|
+
periodicScanRequested = false;
|
|
489
|
+
runPeriodicScan();
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
};
|
|
493
|
+
const ownershipScanTimer = darwinTracker ? setInterval(runPeriodicScan, 100) : undefined;
|
|
494
|
+
// Scan as soon as the supervisor appends a ledger record instead of waiting
|
|
495
|
+
// for the next tick. An incarnation-only record's descendants are only
|
|
496
|
+
// reachable while that child is alive, so the anchor must be taken promptly.
|
|
497
|
+
let ledgerWatcher: fsSync.FSWatcher | undefined;
|
|
498
|
+
if (darwinTracker) {
|
|
499
|
+
try {
|
|
500
|
+
ledgerWatcher = fsSync.watch(ownershipFilePath, runPeriodicScan);
|
|
501
|
+
ledgerWatcher.on("error", () => {});
|
|
502
|
+
} catch {
|
|
503
|
+
ledgerWatcher = undefined;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
357
506
|
const cleanup = (): Promise<void> => {
|
|
358
507
|
cleaning ??= (async () => {
|
|
359
508
|
if (ownershipScanTimer) clearInterval(ownershipScanTimer);
|
|
509
|
+
ledgerWatcher?.close();
|
|
360
510
|
let trackingOk = await scanOwnership();
|
|
361
511
|
if (supervisor.exitCode === null && supervisor.signalCode === null) {
|
|
362
512
|
if (process.platform !== "win32" && supervisor.pid) {
|