@code-yeongyu/senpi 2026.10.11 → 2026.10.12
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 +46 -0
- package/dist/bundle/chunks/anthropic-messages-GYG6YZY5.js +10 -0
- package/dist/bundle/chunks/{app-server-command-NHDOL4HQ.js → app-server-command-OL6PPUOA.js} +1 -1
- package/dist/bundle/chunks/{azure-openai-responses-EJF2XJ7Q.js → azure-openai-responses-GU6FOSJR.js} +1 -1
- package/dist/bundle/chunks/{chunk-ZABLYCHT.js → chunk-46JEQXD7.js} +1 -1
- package/dist/bundle/chunks/{chunk-VE3D74HN.js → chunk-55VKYA5V.js} +1 -1
- package/dist/bundle/chunks/{chunk-7UNN3YHW.js → chunk-5AVFHHDF.js} +230 -221
- package/dist/bundle/chunks/{chunk-2JE2DOIF.js → chunk-5DPTM5E2.js} +1 -1
- package/dist/bundle/chunks/chunk-5UPUSGP5.js +2 -0
- package/dist/bundle/chunks/chunk-5VMRMY6N.js +16 -0
- package/dist/bundle/chunks/{chunk-7X3VRAM3.js → chunk-7P3QPVTF.js} +1 -1
- package/dist/bundle/chunks/{chunk-T6IYKWIH.js → chunk-BUGZZVQW.js} +1 -1
- package/dist/bundle/chunks/chunk-DL7EFVT2.js +12 -0
- package/dist/bundle/chunks/chunk-E5YUVGLQ.js +11 -0
- package/dist/bundle/chunks/{chunk-RLFQICEY.js → chunk-EQJY2B5F.js} +1 -1
- package/dist/bundle/chunks/{chunk-VYKBN4MR.js → chunk-GC2RU5TS.js} +1 -1
- package/dist/bundle/chunks/chunk-GPKEJOTQ.js +202 -0
- package/dist/bundle/chunks/{chunk-KCVSSFOV.js → chunk-GQ5PWY6P.js} +4 -3
- package/dist/bundle/chunks/{chunk-4GPJOGXZ.js → chunk-I4AEC6MH.js} +1 -1
- package/dist/bundle/chunks/chunk-JXSFGRBE.js +8 -0
- package/dist/bundle/chunks/{chunk-YGCMN2CX.js → chunk-NAY5LB53.js} +1 -1
- package/dist/bundle/chunks/{chunk-EBOD66AS.js → chunk-PBN2CATL.js} +2 -2
- package/dist/bundle/chunks/chunk-PFNBY43W.js +2 -0
- package/dist/bundle/chunks/{chunk-CY2B4QRR.js → chunk-PUWEQK2W.js} +1 -1
- package/dist/bundle/chunks/{chunk-5DBPUQSX.js → chunk-SF6AUNQZ.js} +1 -1
- package/dist/bundle/chunks/{chunk-G2XAU4BX.js → chunk-TMTT5CLA.js} +1 -1
- package/dist/bundle/chunks/{chunk-2JKDZXIY.js → chunk-VWFKLW73.js} +2 -2
- package/dist/bundle/chunks/chunk-WZMSBRTW.js +2 -0
- package/dist/bundle/chunks/{chunk-55HLKZSB.js → chunk-XVJRRLGN.js} +1 -1
- package/dist/bundle/chunks/chunk-Z2PWWBBD.js +31 -0
- package/dist/bundle/chunks/cli-main-RSIYAR4J.js +3 -0
- package/dist/bundle/chunks/{cloudflare-workers-ai-system-one-SWVU2AXJ.js → cloudflare-workers-ai-system-one-LVWRKOUD.js} +1 -1
- package/dist/bundle/chunks/{google-generative-ai-XG4YX6FQ.js → google-generative-ai-JMGYPTXL.js} +1 -1
- package/dist/bundle/chunks/{google-vertex-BBOZ6OUB.js → google-vertex-CI4FPIU7.js} +1 -1
- package/dist/bundle/chunks/{help-fast-path-EZ3KEW6V.js → help-fast-path-M7KMBM6K.js} +1 -1
- package/dist/bundle/chunks/host-command-6UR3DSFR.js +2 -0
- package/dist/bundle/chunks/host-lifecycle-2BKCCK5X.js +3 -0
- package/dist/bundle/chunks/host-lifecycle.js +10 -7
- package/dist/bundle/chunks/host-session-command-A2MZHEBP.js +19 -0
- package/dist/bundle/chunks/interactive-mode-EJBMTJOH.js +2 -0
- package/dist/bundle/chunks/{list-tips-WE2YGQM6.js → list-tips-BEQAOA4N.js} +1 -1
- package/dist/bundle/chunks/{mistral-conversations-RB3ZBCGE.js → mistral-conversations-5OD7662T.js} +1 -1
- package/dist/bundle/chunks/{multi-session-host-V4CUQLV7.js → multi-session-host-YYMLHCUA.js} +4 -4
- package/dist/bundle/chunks/{openai-codex-responses-DBTPUXRW.js → openai-codex-responses-FN3UPAFI.js} +1 -1
- package/dist/bundle/chunks/{openai-completions-QAJQC4BY.js → openai-completions-KMUQ3T4M.js} +1 -1
- package/dist/bundle/chunks/{openai-responses-UBJWCEHG.js → openai-responses-V4Q5GVLO.js} +1 -1
- package/dist/bundle/chunks/{package-manager-cli-WTRDT4ZH.js → package-manager-cli-6A5ZP26E.js} +1 -1
- package/dist/bundle/chunks/rotation-stream-PT6LLBMJ.js +2 -0
- package/dist/bundle/chunks/rpc-mode-XNYJPYUM.js +2 -0
- package/dist/bundle/chunks/runtime-session-ZBHFMT65.js +2 -0
- package/dist/bundle/chunks/{schedule-command-PJVRKAKD.js → schedule-command-FYQG2MFX.js} +1 -1
- package/dist/bundle/chunks/session-control-endpoint-7TX4XHN4.js +3 -0
- package/dist/bundle/chunks/session-picker-4RNB27SM.js +2 -0
- package/dist/bundle/chunks/session-worker.js +418 -412
- package/dist/bundle/chunks/{typesafe-system-one-B5TW3FHM.js → typesafe-system-one-WUHW7HR4.js} +1 -1
- package/dist/bundle/cli.js +1 -1
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/bundle/runtime-manifest.json +1 -1
- package/dist/core/agent-session.d.ts +11 -2
- package/dist/core/agent-session.js +152 -43
- package/dist/core/compaction/compaction.js +2 -0
- package/dist/core/extensions/builtin/anthropic-web-search/index.js +22 -14
- package/dist/core/extensions/builtin/cache-keepalive/index.js +8 -4
- package/dist/core/extensions/builtin/cache-keepalive/session-prewarm.d.ts +2 -1
- package/dist/core/extensions/builtin/cache-keepalive/session-prewarm.js +55 -7
- package/dist/core/extensions/builtin/cache-keepalive/startup-activity-gate.d.ts +8 -0
- package/dist/core/extensions/builtin/cache-keepalive/startup-activity-gate.js +25 -0
- package/dist/core/extensions/builtin/compaction/index.js +13 -5
- package/dist/core/extensions/builtin/compaction/speculative.d.ts +1 -0
- package/dist/core/extensions/builtin/compaction/speculative.js +8 -4
- package/dist/core/extensions/builtin/moved-path-guard/tool-classes.js +1 -0
- package/dist/core/extensions/builtin/openai-web-search/index.d.ts +4 -1
- package/dist/core/extensions/builtin/openai-web-search/index.js +54 -17
- package/dist/core/extensions/builtin/server-search-fallback.d.ts +12 -0
- package/dist/core/extensions/builtin/server-search-fallback.js +50 -0
- package/dist/core/extensions/builtin/terminal/runtime-session.d.ts +10 -1
- package/dist/core/extensions/builtin/terminal/runtime-session.js +48 -4
- package/dist/core/extensions/builtin/websearch/index.d.ts +1 -1
- package/dist/core/extensions/builtin/websearch/index.js +119 -18
- package/dist/core/extensions/builtin/websearch/server-search.d.ts +11 -0
- package/dist/core/extensions/builtin/websearch/server-search.js +230 -0
- package/dist/core/extensions/builtin/websearch/websearch/native.d.ts +2 -0
- package/dist/core/extensions/builtin/websearch/websearch/search-model.js +15 -0
- package/dist/core/extensions/runner.d.ts +8 -1
- package/dist/core/extensions/runner.js +56 -7
- package/dist/core/extensions/types.d.ts +12 -2
- package/dist/core/extensions/user-activity.d.ts +10 -0
- package/dist/core/extensions/user-activity.js +20 -0
- package/dist/core/model-config-schema.d.ts +34 -0
- package/dist/core/model-config-schema.js +2 -0
- package/dist/core/model-config.js +1 -0
- package/dist/core/reserved-keywords.d.ts +28 -0
- package/dist/core/reserved-keywords.js +81 -0
- package/dist/core/retry-fallback/candidates.d.ts +1 -1
- package/dist/core/retry-fallback/candidates.js +1 -12
- package/dist/core/retry-fallback/controller.d.ts +3 -1
- package/dist/core/retry-fallback/controller.js +11 -5
- package/dist/core/retry-fallback/cooldown.d.ts +2 -1
- package/dist/core/retry-fallback/cooldown.js +21 -12
- package/dist/core/retry-fallback/unusable.d.ts +2 -0
- package/dist/core/retry-fallback/unusable.js +14 -2
- package/dist/core/retry-fallback/usage-limit.d.ts +10 -1
- package/dist/core/retry-fallback/usage-limit.js +39 -2
- package/dist/core/session-title-generator.js +2 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/modes/index.d.ts +1 -1
- package/dist/modes/interactive/components/assistant-render-descriptors.js +7 -1
- package/dist/modes/interactive/composer-user-activity.d.ts +6 -0
- package/dist/modes/interactive/composer-user-activity.js +30 -0
- package/dist/modes/interactive/grok/chrome.js +2 -1
- package/dist/modes/interactive/interactive-mode.js +15 -6
- package/dist/modes/interactive/session-control-registry.js +8 -1
- package/dist/modes/interactive/theme/reserved-keyword-rainbow.d.ts +11 -0
- package/dist/modes/interactive/theme/reserved-keyword-rainbow.js +50 -0
- package/dist/modes/interactive/theme/theme.d.ts +3 -1
- package/dist/modes/interactive/theme/theme.js +9 -0
- package/dist/modes/print-mode.js +7 -2
- package/dist/modes/rpc/connection-handler.js +43 -11
- package/dist/modes/rpc/custom-capability.d.ts +2 -0
- package/dist/modes/rpc/custom-capability.js +2 -0
- package/dist/modes/rpc/host-admission-proxy.d.ts +9 -0
- package/dist/modes/rpc/host-admission-proxy.js +211 -0
- package/dist/modes/rpc/host-admission.d.ts +21 -0
- package/dist/modes/rpc/host-admission.js +59 -0
- package/dist/modes/rpc/host-capabilities.js +2 -1
- package/dist/modes/rpc/host-client-occupancy.d.ts +2 -1
- package/dist/modes/rpc/host-client-occupancy.js +7 -0
- package/dist/modes/rpc/host-decision.d.ts +2 -2
- package/dist/modes/rpc/host-decision.js +4 -0
- package/dist/modes/rpc/host-ensure-types.d.ts +2 -0
- package/dist/modes/rpc/host-ensure.js +31 -3
- package/dist/modes/rpc/host-lifecycle-drain.d.ts +2 -1
- package/dist/modes/rpc/host-lifecycle-drain.js +3 -1
- package/dist/modes/rpc/host-lifecycle-proxy.d.ts +3 -2
- package/dist/modes/rpc/host-lifecycle-proxy.js +10 -19
- package/dist/modes/rpc/host-lifecycle-shutdown.d.ts +2 -0
- package/dist/modes/rpc/host-lifecycle-shutdown.js +3 -1
- package/dist/modes/rpc/host-lifecycle-ticker.d.ts +11 -0
- package/dist/modes/rpc/host-lifecycle-ticker.js +27 -0
- package/dist/modes/rpc/host-lifecycle.js +15 -23
- package/dist/modes/rpc/host-outcome.js +1 -0
- package/dist/modes/rpc/host-protocol-info.d.ts +2 -0
- package/dist/modes/rpc/host-protocol-info.js +1 -0
- package/dist/modes/rpc/host-session-runner.js +22 -1
- package/dist/modes/rpc/host-status-all.js +1 -0
- package/dist/modes/rpc/host-status.d.ts +1 -0
- package/dist/modes/rpc/host-status.js +5 -1
- package/dist/modes/rpc/rpc-client.d.ts +7 -1
- package/dist/modes/rpc/rpc-client.js +12 -0
- package/dist/modes/rpc/rpc-types.d.ts +49 -1
- package/dist/modes/rpc/rpc-types.js +5 -0
- package/dist/modes/rpc/session-command-activity.js +2 -0
- package/dist/modes/rpc/session-held.js +3 -0
- package/docs/extensions.md +13 -1
- package/docs/providers.md +88 -0
- package/docs/rpc.md +150 -5
- package/docs/web-search.md +3 -1
- package/package.json +7 -7
- package/dist/bundle/chunks/anthropic-messages-5QQJVQPZ.js +0 -9
- package/dist/bundle/chunks/chunk-2A3MS2Z7.js +0 -8
- package/dist/bundle/chunks/chunk-6SBY4ZWC.js +0 -2
- package/dist/bundle/chunks/chunk-6WCOSBI7.js +0 -2
- package/dist/bundle/chunks/chunk-E75RO54O.js +0 -14
- package/dist/bundle/chunks/chunk-JMYO34RK.js +0 -2
- package/dist/bundle/chunks/chunk-KRNXUTZJ.js +0 -2
- package/dist/bundle/chunks/chunk-L4HVJWIF.js +0 -11
- package/dist/bundle/chunks/chunk-N5WLLHUE.js +0 -2
- package/dist/bundle/chunks/chunk-NEFNNQSV.js +0 -9
- package/dist/bundle/chunks/chunk-PWIYVLIA.js +0 -9
- package/dist/bundle/chunks/chunk-Q3R44CGT.js +0 -5
- package/dist/bundle/chunks/chunk-QIARZN7G.js +0 -31
- package/dist/bundle/chunks/chunk-QKU6JOA4.js +0 -2
- package/dist/bundle/chunks/chunk-QYWXDE47.js +0 -2
- package/dist/bundle/chunks/chunk-XHELSLN3.js +0 -188
- package/dist/bundle/chunks/chunk-YDJ4HGWO.js +0 -16
- package/dist/bundle/chunks/chunk-ZJHRDQCH.js +0 -2
- package/dist/bundle/chunks/cli-main-TUBNBZNK.js +0 -3
- package/dist/bundle/chunks/host-command-WO7VKJBJ.js +0 -2
- package/dist/bundle/chunks/host-lifecycle-X2J5FTJL.js +0 -3
- package/dist/bundle/chunks/host-session-command-ICBOH5RP.js +0 -19
- package/dist/bundle/chunks/interactive-mode-UGDNCGQ5.js +0 -2
- package/dist/bundle/chunks/rotation-stream-OODURUGZ.js +0 -2
- package/dist/bundle/chunks/rpc-mode-22MCT32O.js +0 -2
- package/dist/bundle/chunks/runtime-session-I4NDNGC5.js +0 -2
- package/dist/bundle/chunks/session-control-endpoint-SFPRQBDO.js +0 -3
- package/dist/bundle/chunks/session-picker-3ZQPV7Z3.js +0 -2
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { StringDecoder } from "node:string_decoder";
|
|
2
|
+
import { hostStoppingResponse } from "./host-admission.js";
|
|
3
|
+
import { MAX_RPC_LINE_CHARACTERS, serializeJsonLine } from "./jsonl.js";
|
|
4
|
+
/**
|
|
5
|
+
* Requests are size-bounded; each direction awaits downstream socket backpressure.
|
|
6
|
+
* Opens remain concurrent with control commands. Stop refuses every pending open,
|
|
7
|
+
* while other commands retain their wire order and child-owned semantics.
|
|
8
|
+
*/
|
|
9
|
+
export function proxyWithAdmission(client, internal, admission) {
|
|
10
|
+
const pending = [];
|
|
11
|
+
const refused = new Set();
|
|
12
|
+
let protocols = 0;
|
|
13
|
+
let refusals = Promise.resolve();
|
|
14
|
+
const unsubscribe = admission.onStop(() => {
|
|
15
|
+
refusals = refusePending();
|
|
16
|
+
return refusals;
|
|
17
|
+
});
|
|
18
|
+
async function refusePending() {
|
|
19
|
+
const ids = pending.splice(0);
|
|
20
|
+
for (const id of ids)
|
|
21
|
+
refused.add(id);
|
|
22
|
+
try {
|
|
23
|
+
for (const id of ids)
|
|
24
|
+
await write(client, serializeJsonLine(hostStoppingResponse(id, "open_session", admission)));
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
fail();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
client.once("close", () => {
|
|
31
|
+
unsubscribe();
|
|
32
|
+
internal?.destroy();
|
|
33
|
+
});
|
|
34
|
+
const fail = () => {
|
|
35
|
+
internal?.destroy();
|
|
36
|
+
client.destroy();
|
|
37
|
+
};
|
|
38
|
+
client.once("error", fail);
|
|
39
|
+
internal?.once("error", () => {
|
|
40
|
+
if (!admission.stopping)
|
|
41
|
+
fail();
|
|
42
|
+
});
|
|
43
|
+
void requests().catch(() => {
|
|
44
|
+
if (!admission.stopping)
|
|
45
|
+
fail();
|
|
46
|
+
});
|
|
47
|
+
if (internal)
|
|
48
|
+
void responses(internal).catch(() => {
|
|
49
|
+
if (!admission.stopping)
|
|
50
|
+
fail();
|
|
51
|
+
});
|
|
52
|
+
async function requests() {
|
|
53
|
+
for await (const line of lines(client, MAX_RPC_LINE_CHARACTERS)) {
|
|
54
|
+
if (line === undefined) {
|
|
55
|
+
await write(client, serializeJsonLine({
|
|
56
|
+
type: "response",
|
|
57
|
+
command: "parse",
|
|
58
|
+
success: false,
|
|
59
|
+
error: `RPC input line exceeds ${MAX_RPC_LINE_CHARACTERS} characters.`,
|
|
60
|
+
}));
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const request = record(line);
|
|
64
|
+
if (admission.stopping && request?.type === "get_protocol_info") {
|
|
65
|
+
await write(client, serializeJsonLine({
|
|
66
|
+
id: request.id,
|
|
67
|
+
type: "response",
|
|
68
|
+
command: request.type,
|
|
69
|
+
success: true,
|
|
70
|
+
data: admission.protocol(),
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
else if (admission.stopping && (request?.type === "open_session" || !internal)) {
|
|
74
|
+
await write(client, serializeJsonLine(hostStoppingResponse(request?.id, String(request?.type ?? "parse"), admission)));
|
|
75
|
+
}
|
|
76
|
+
else if (internal) {
|
|
77
|
+
// Record admission before writing: a child response can arrive immediately.
|
|
78
|
+
if (request?.type === "open_session")
|
|
79
|
+
pending.push(request.id);
|
|
80
|
+
if (request?.type === "get_protocol_info")
|
|
81
|
+
protocols++;
|
|
82
|
+
await write(internal, `${line}\n`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
internal?.end();
|
|
86
|
+
}
|
|
87
|
+
async function responses(source) {
|
|
88
|
+
// Keep complete records together so local refusals cannot split a streaming delta.
|
|
89
|
+
// After admission, ordinary traffic stays as raw bytes with one write per chunk.
|
|
90
|
+
let fragments = [];
|
|
91
|
+
for await (const chunk of source) {
|
|
92
|
+
const newline = chunk.lastIndexOf(0x0a);
|
|
93
|
+
if (newline === -1) {
|
|
94
|
+
fragments.push(chunk);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const complete = chunk.subarray(0, newline + 1);
|
|
98
|
+
if (pending.length === 0 && protocols === 0 && refused.size === 0) {
|
|
99
|
+
// Queue the entire record batch synchronously, without copying split buffers.
|
|
100
|
+
client.cork();
|
|
101
|
+
let written;
|
|
102
|
+
try {
|
|
103
|
+
for (const fragment of fragments)
|
|
104
|
+
client.write(fragment);
|
|
105
|
+
written = write(client, complete);
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
client.uncork();
|
|
109
|
+
}
|
|
110
|
+
await written;
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
const batch = fragments.length > 0 ? Buffer.concat([...fragments, complete]) : complete;
|
|
114
|
+
await write(client, filterReplies(batch));
|
|
115
|
+
}
|
|
116
|
+
fragments = newline + 1 < chunk.length ? [chunk.subarray(newline + 1)] : [];
|
|
117
|
+
}
|
|
118
|
+
if (fragments.length > 0)
|
|
119
|
+
await write(client, filterReplies(Buffer.concat(fragments)));
|
|
120
|
+
await refusals;
|
|
121
|
+
client.end(() => client.destroy());
|
|
122
|
+
}
|
|
123
|
+
function filterReplies(batch) {
|
|
124
|
+
if (pending.length === 0 && protocols === 0 && refused.size === 0)
|
|
125
|
+
return batch;
|
|
126
|
+
const output = [];
|
|
127
|
+
let changed = false;
|
|
128
|
+
for (let start = 0; start < batch.length;) {
|
|
129
|
+
const newline = batch.indexOf(0x0a, start);
|
|
130
|
+
const end = newline === -1 ? batch.length : newline + 1;
|
|
131
|
+
const line = batch.subarray(start, end);
|
|
132
|
+
start = end;
|
|
133
|
+
// Only lifecycle responses need decoding; histories and deltas remain byte slices.
|
|
134
|
+
const response = line.includes('"open_session"') || line.includes('"get_protocol_info"')
|
|
135
|
+
? record(line.toString("utf8"))
|
|
136
|
+
: undefined;
|
|
137
|
+
if (response?.type === "response" && response.command === "open_session") {
|
|
138
|
+
if (refused.has(response.id)) {
|
|
139
|
+
changed = true;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const index = pending.indexOf(response.id);
|
|
143
|
+
if (index !== -1)
|
|
144
|
+
pending.splice(index, 1);
|
|
145
|
+
}
|
|
146
|
+
if (response?.type === "response" && response.command === "get_protocol_info") {
|
|
147
|
+
protocols = Math.max(0, protocols - 1);
|
|
148
|
+
if (response.success === true) {
|
|
149
|
+
if (isRecord(response.data))
|
|
150
|
+
admission.observe(response.data);
|
|
151
|
+
output.push(Buffer.from(serializeJsonLine({ ...response, data: admission.protocol() })));
|
|
152
|
+
changed = true;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
output.push(line);
|
|
157
|
+
}
|
|
158
|
+
return changed ? Buffer.concat(output) : batch;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
async function write(socket, line) {
|
|
162
|
+
await new Promise((resolve, reject) => {
|
|
163
|
+
socket.write(line, (error) => (error ? reject(error) : resolve()));
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
/** Strict LF and incremental UTF-8; an oversized input is discarded through its next LF. */
|
|
167
|
+
async function* lines(socket, limit) {
|
|
168
|
+
const decoder = new StringDecoder("utf8");
|
|
169
|
+
let buffer = "";
|
|
170
|
+
let discarding = false;
|
|
171
|
+
for await (const chunk of socket) {
|
|
172
|
+
const text = typeof chunk === "string" ? chunk : decoder.write(chunk);
|
|
173
|
+
let offset = 0;
|
|
174
|
+
while (offset < text.length) {
|
|
175
|
+
const newline = text.indexOf("\n", offset);
|
|
176
|
+
const end = newline === -1 ? text.length : newline;
|
|
177
|
+
if (!discarding) {
|
|
178
|
+
if (buffer.length + end - offset > limit) {
|
|
179
|
+
buffer = "";
|
|
180
|
+
discarding = true;
|
|
181
|
+
yield undefined;
|
|
182
|
+
}
|
|
183
|
+
else
|
|
184
|
+
buffer += text.slice(offset, end);
|
|
185
|
+
}
|
|
186
|
+
if (newline === -1)
|
|
187
|
+
break;
|
|
188
|
+
if (!discarding)
|
|
189
|
+
yield buffer;
|
|
190
|
+
buffer = "";
|
|
191
|
+
discarding = false;
|
|
192
|
+
offset = newline + 1;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
buffer += decoder.end();
|
|
196
|
+
if (!discarding && buffer.length > 0)
|
|
197
|
+
yield buffer;
|
|
198
|
+
}
|
|
199
|
+
function record(line) {
|
|
200
|
+
try {
|
|
201
|
+
const value = JSON.parse(line);
|
|
202
|
+
return isRecord(value) ? value : undefined;
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function isRecord(value) {
|
|
209
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
210
|
+
}
|
|
211
|
+
//# sourceMappingURL=host-admission-proxy.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The public admission authority lives on the supervisor's loop. A ready probe is
|
|
3
|
+
* informational: every open must still pass this gate, including on held sockets.
|
|
4
|
+
*/
|
|
5
|
+
export declare class HostAdmission {
|
|
6
|
+
private stopped;
|
|
7
|
+
private readonly listeners;
|
|
8
|
+
private replies;
|
|
9
|
+
private info;
|
|
10
|
+
constructor(info: Readonly<Record<string, unknown>>);
|
|
11
|
+
get stopping(): boolean;
|
|
12
|
+
protocol(): Readonly<Record<string, unknown>>;
|
|
13
|
+
observe(info: Readonly<Record<string, unknown>>): void;
|
|
14
|
+
/** Synchronous and monotonic; runs before any child signal or awaited teardown. */
|
|
15
|
+
stop(): void;
|
|
16
|
+
/** Flush within the caller's existing five-second cleanup allowance; a stalled reader cannot pin exit. */
|
|
17
|
+
flush(): Promise<void>;
|
|
18
|
+
onStop(listener: () => Promise<void>): () => void;
|
|
19
|
+
}
|
|
20
|
+
export declare function hostStoppingResponse(id: unknown, command: string, admission: HostAdmission): object;
|
|
21
|
+
//# sourceMappingURL=host-admission.d.ts.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The public admission authority lives on the supervisor's loop. A ready probe is
|
|
3
|
+
* informational: every open must still pass this gate, including on held sockets.
|
|
4
|
+
*/
|
|
5
|
+
export class HostAdmission {
|
|
6
|
+
constructor(info) {
|
|
7
|
+
this.stopped = false;
|
|
8
|
+
this.listeners = new Set();
|
|
9
|
+
this.replies = Promise.resolve();
|
|
10
|
+
this.info = info;
|
|
11
|
+
}
|
|
12
|
+
get stopping() {
|
|
13
|
+
return this.stopped;
|
|
14
|
+
}
|
|
15
|
+
protocol() {
|
|
16
|
+
return { ...this.info, lifecycle: this.stopped ? "stopping" : "ready" };
|
|
17
|
+
}
|
|
18
|
+
observe(info) {
|
|
19
|
+
this.info = info;
|
|
20
|
+
}
|
|
21
|
+
/** Synchronous and monotonic; runs before any child signal or awaited teardown. */
|
|
22
|
+
stop() {
|
|
23
|
+
if (this.stopped)
|
|
24
|
+
return;
|
|
25
|
+
this.stopped = true;
|
|
26
|
+
this.replies = Promise.all([...this.listeners].map((listener) => listener())).then(() => { });
|
|
27
|
+
}
|
|
28
|
+
/** Flush within the caller's existing five-second cleanup allowance; a stalled reader cannot pin exit. */
|
|
29
|
+
async flush() {
|
|
30
|
+
let timer;
|
|
31
|
+
try {
|
|
32
|
+
await Promise.race([
|
|
33
|
+
this.replies,
|
|
34
|
+
new Promise((resolve) => {
|
|
35
|
+
timer = setTimeout(resolve, 2_000);
|
|
36
|
+
}),
|
|
37
|
+
]);
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
clearTimeout(timer);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
onStop(listener) {
|
|
44
|
+
this.listeners.add(listener);
|
|
45
|
+
return () => this.listeners.delete(listener);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function hostStoppingResponse(id, command, admission) {
|
|
49
|
+
return {
|
|
50
|
+
...(typeof id === "string" ? { id } : {}),
|
|
51
|
+
type: "response",
|
|
52
|
+
command,
|
|
53
|
+
success: false,
|
|
54
|
+
error: "host_stopping",
|
|
55
|
+
errorCode: "host_stopping",
|
|
56
|
+
errorData: { instanceId: admission.protocol().instanceId },
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=host-admission.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ACCEPT_EDITS_PERMISSION_PRESET_CAPABILITY, AUTO_PERMISSION_PRESET_CAPABILITY, } from "../../core/extensions/builtin/permission-system/config.js";
|
|
2
2
|
import { DURABLE_CLIENT_MESSAGE_ID_CAPABILITY } from "./client-admission-record.js";
|
|
3
|
-
import { AUTO_TITLE_PER_SESSION_CAPABILITY, AUTO_TITLE_SESSIONS_CAPABILITY, BROWSER_ENGINE_CAPABILITY, CONTINUE_FROM_LEAF_CAPABILITY, DURABLE_SESSION_ID_CAPABILITY, MEDIA_PLACEHOLDERS_CAPABILITY, MOVED_PATH_GUARD_CAPABILITY, PROMPT_SURFACE_CAPABILITY, PROMPT_SURFACE_CHAT_CAPABILITY, RETAIN_ON_DISCONNECT_CAPABILITY, RETRY_FALLBACK_PROFILE_CAPABILITY, SESSION_CONTEXT_CAPABILITY, SESSION_HELD_CAPABILITY, SESSION_KIND_CAPABILITY, WARM_CAPABILITY, } from "./custom-capability.js";
|
|
3
|
+
import { AUTO_TITLE_PER_SESSION_CAPABILITY, AUTO_TITLE_SESSIONS_CAPABILITY, BROWSER_ENGINE_CAPABILITY, CONTINUE_FROM_LEAF_CAPABILITY, DURABLE_SESSION_ID_CAPABILITY, EVAL_CELLS_CAPABILITY, MEDIA_PLACEHOLDERS_CAPABILITY, MOVED_PATH_GUARD_CAPABILITY, PROMPT_SURFACE_CAPABILITY, PROMPT_SURFACE_CHAT_CAPABILITY, RETAIN_ON_DISCONNECT_CAPABILITY, RETRY_FALLBACK_PROFILE_CAPABILITY, SESSION_CONTEXT_CAPABILITY, SESSION_HELD_CAPABILITY, SESSION_KIND_CAPABILITY, WARM_CAPABILITY, } from "./custom-capability.js";
|
|
4
4
|
/**
|
|
5
5
|
* The capabilities a multi-session host advertises in `get_protocol_info`, in their wire order. `warm` is added only
|
|
6
6
|
* when the runtime is in-process, and `negotiated` carries the launch capabilities this connection was given.
|
|
@@ -13,6 +13,7 @@ export function multiSessionHostCapabilities(options) {
|
|
|
13
13
|
MEDIA_PLACEHOLDERS_CAPABILITY,
|
|
14
14
|
DURABLE_CLIENT_MESSAGE_ID_CAPABILITY,
|
|
15
15
|
CONTINUE_FROM_LEAF_CAPABILITY,
|
|
16
|
+
EVAL_CELLS_CAPABILITY,
|
|
16
17
|
// Both shared runtimes dispatch through the active session-writing holder guard.
|
|
17
18
|
SESSION_HELD_CAPABILITY,
|
|
18
19
|
// Host capabilities, not client opt-ins: only a multi-session host owns the
|
|
@@ -14,10 +14,11 @@ export declare class ClientOccupancy {
|
|
|
14
14
|
private readonly observers;
|
|
15
15
|
private readonly attached;
|
|
16
16
|
private readonly onAttach;
|
|
17
|
+
private closed;
|
|
17
18
|
constructor(onAttach: () => void);
|
|
18
19
|
get attachedCount(): number;
|
|
19
20
|
get unclassifiedCount(): number;
|
|
20
|
-
admit(client: Socket):
|
|
21
|
+
admit(client: Socket): boolean;
|
|
21
22
|
/** Returns whether this peer left without ever proving it was an observing read. */
|
|
22
23
|
release(client: Socket): boolean;
|
|
23
24
|
destroyAll(): void;
|
|
@@ -6,6 +6,7 @@ export class ClientOccupancy {
|
|
|
6
6
|
this.clients = new Set();
|
|
7
7
|
this.observers = new Set();
|
|
8
8
|
this.attached = new Set();
|
|
9
|
+
this.closed = false;
|
|
9
10
|
this.onAttach = onAttach;
|
|
10
11
|
}
|
|
11
12
|
get attachedCount() {
|
|
@@ -15,6 +16,10 @@ export class ClientOccupancy {
|
|
|
15
16
|
return this.clients.size - this.observers.size - this.attached.size;
|
|
16
17
|
}
|
|
17
18
|
admit(client) {
|
|
19
|
+
if (this.closed) {
|
|
20
|
+
client.destroy();
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
18
23
|
this.clients.add(client);
|
|
19
24
|
let pending = Buffer.alloc(0);
|
|
20
25
|
const attach = () => {
|
|
@@ -39,6 +44,7 @@ export class ClientOccupancy {
|
|
|
39
44
|
attach();
|
|
40
45
|
};
|
|
41
46
|
client.on("data", classify);
|
|
47
|
+
return true;
|
|
42
48
|
}
|
|
43
49
|
/** Returns whether this peer left without ever proving it was an observing read. */
|
|
44
50
|
release(client) {
|
|
@@ -49,6 +55,7 @@ export class ClientOccupancy {
|
|
|
49
55
|
return unclassified;
|
|
50
56
|
}
|
|
51
57
|
destroyAll() {
|
|
58
|
+
this.closed = true;
|
|
52
59
|
for (const client of this.clients)
|
|
53
60
|
client.destroy();
|
|
54
61
|
}
|
|
@@ -62,7 +62,7 @@ export type HostDecision = {
|
|
|
62
62
|
readonly upgradeable: false;
|
|
63
63
|
} | {
|
|
64
64
|
readonly action: "refuse";
|
|
65
|
-
readonly reason: "protocol" | "capability";
|
|
65
|
+
readonly reason: "protocol" | "capability" | "host_stopping";
|
|
66
66
|
readonly upgradeable: false;
|
|
67
67
|
} | {
|
|
68
68
|
readonly action: "fallback";
|
|
@@ -85,7 +85,7 @@ export type AttachOnlyDecision = Extract<HostDecision, {
|
|
|
85
85
|
action: "start" | "reuse" | "refuse";
|
|
86
86
|
}>;
|
|
87
87
|
/** Why an ensure refused. The decision's own refusals plus the one only a pidfile can produce. */
|
|
88
|
-
export type HostRefusalReason = "protocol" | "capability" | "foreign_writer" | "legacy_host" | "host_busy" | "host_stalled";
|
|
88
|
+
export type HostRefusalReason = "protocol" | "capability" | "foreign_writer" | "legacy_host" | "host_busy" | "host_stopping" | "host_stalled";
|
|
89
89
|
/**
|
|
90
90
|
* An ensure that found an unusable host and refused to act on it. A refusal is FINAL by design:
|
|
91
91
|
* this process may not signal a host it did not start (I1), and starting a second host on an
|
|
@@ -63,6 +63,8 @@ function refusalDetail(reason, host) {
|
|
|
63
63
|
return "a host from before the per-socket daemon directory is still running; it is drained and replaced only while it holds no session, and no second host is started beside it";
|
|
64
64
|
case "host_busy":
|
|
65
65
|
return "its socket accepts connections but did not answer inside the probe budget: a live host under load, which is never ended to make room for a replacement";
|
|
66
|
+
case "host_stopping":
|
|
67
|
+
return "this generation is stopping; wait for its exit before launching a successor";
|
|
66
68
|
case "host_stalled":
|
|
67
69
|
return "nothing reaches its socket, but its process is alive and its own loop-lag watchdog measured a recent stall: a stalled host is still serving its sessions, so it is neither stopped nor replaced";
|
|
68
70
|
default:
|
|
@@ -73,6 +75,8 @@ export function decideHostAction(client, host, policy) {
|
|
|
73
75
|
if (host === undefined) {
|
|
74
76
|
return { action: "start", reason: client.startedByUs ? "restart_own_host" : "no_host", upgradeable: false };
|
|
75
77
|
}
|
|
78
|
+
if (host.lifecycle === "stopping")
|
|
79
|
+
return { action: "refuse", reason: "host_stopping", upgradeable: false };
|
|
76
80
|
if (host.protocolVersion !== client.protocolVersion) {
|
|
77
81
|
return { action: "refuse", reason: "protocol", upgradeable: false };
|
|
78
82
|
}
|
|
@@ -22,6 +22,8 @@ export interface EnsureHostOptions {
|
|
|
22
22
|
readonly env?: Readonly<Record<string, string | null>>;
|
|
23
23
|
/** Whether a newer build may take the socket over from the running host. Defaults to `never`. */
|
|
24
24
|
readonly upgrade?: HostUpgradePolicy;
|
|
25
|
+
/** Maximum wait for an already-stopping generation; capped by the normal stop budget. */
|
|
26
|
+
readonly stoppingTimeoutMs?: number;
|
|
25
27
|
readonly _test?: {
|
|
26
28
|
readonly readinessTimeoutMs?: number;
|
|
27
29
|
readonly stopTimeoutMs?: number;
|
|
@@ -10,7 +10,7 @@ import { ensureClient } from "./host-ensure-client.js";
|
|
|
10
10
|
import { publicEndpointAccepts, refuseIfStalled } from "./host-ensure-liveness.js";
|
|
11
11
|
import { hostEnsureLockOptions, hostEnsureLockTarget } from "./host-ensure-lock.js";
|
|
12
12
|
import { appendStderr, DEFAULT_READINESS_TIMEOUT_MS, DEFAULT_STOP_TIMEOUT_MS, startHost } from "./host-ensure-start.js";
|
|
13
|
-
import { ensureSender, matchesPidFileOrUnknown, STOP_WAIT_BUDGET_MS, stopManagedHost } from "./host-ensure-stop.js";
|
|
13
|
+
import { delay, ensureSender, matchesPidFileOrUnknown, resolvePidFileOwnership, STOP_WAIT_BUDGET_MS, stopManagedHost, } from "./host-ensure-stop.js";
|
|
14
14
|
import { scheduleOpportunisticHostGc } from "./host-gc-pass.js";
|
|
15
15
|
import { HANDOFF_LOCK_HOLD_MS, handoffHostLocked } from "./host-handoff.js";
|
|
16
16
|
import { reapOrphanedInternalHostDirs } from "./host-internal-dir-reaper.js";
|
|
@@ -67,7 +67,7 @@ export async function ensureHost(options) {
|
|
|
67
67
|
scheduleOpportunisticHostGc({ agentDir: options.agentDir ?? getAgentDir(), exclude: socket });
|
|
68
68
|
return ensured;
|
|
69
69
|
}
|
|
70
|
-
async function ensureHostLocked(paths, socket, options) {
|
|
70
|
+
async function ensureHostLocked(paths, socket, options, stopDeadline = Date.now() + Math.min(options.stoppingTimeoutMs ?? STOP_WAIT_BUDGET_MS, STOP_WAIT_BUDGET_MS)) {
|
|
71
71
|
// Under the lock, so a torn or foreign `endpoint.json` is repaired rather than left unaddressable.
|
|
72
72
|
await ensureEndpointIdentity(paths, socket, { repair: true });
|
|
73
73
|
const testOptions = options._test;
|
|
@@ -84,6 +84,23 @@ async function ensureHostLocked(paths, socket, options) {
|
|
|
84
84
|
// A reusable host is held from the connection that proved it compatible, never re-probed later.
|
|
85
85
|
const held = await holdProtocolInfo(socket, EXISTING_HOST_PROBE_TIMEOUT_MS);
|
|
86
86
|
const protocol = held?.info;
|
|
87
|
+
if (protocol?.lifecycle === "stopping") {
|
|
88
|
+
held?.hold.release();
|
|
89
|
+
// A typed stop is not permission to signal or overwrite a foreign generation.
|
|
90
|
+
// Wait for BOTH identities to be gone, then re-run discovery under this lock.
|
|
91
|
+
if (!registeredHere || !registered || registered.instanceId !== protocol.instanceId)
|
|
92
|
+
throw new HostEnsureRefusedError(socket, "host_stopping", protocol, "no matching registered generation");
|
|
93
|
+
const generation = generationPaths(paths, registered.instanceId);
|
|
94
|
+
for (;;) {
|
|
95
|
+
if ((await resolvePidFileOwnership(registered.record, testOptions?.readProcessStartTime)) === "gone" &&
|
|
96
|
+
!(await hostChildAlive(generation)))
|
|
97
|
+
return ensureHostLocked(paths, socket, options, stopDeadline);
|
|
98
|
+
const remaining = stopDeadline - Date.now();
|
|
99
|
+
if (remaining <= 0)
|
|
100
|
+
throw new HostEnsureRefusedError(socket, "host_stopping", protocol, "generation exit exceeded stop budget");
|
|
101
|
+
await delay(Math.min(50, remaining));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
87
104
|
const startedByUs = registeredHere && (await writtenByThisProcess(registered?.writer, testOptions?.readProcessStartTime));
|
|
88
105
|
const attachedPid = registeredHere ? (registered?.record.pid ?? 0) : 0;
|
|
89
106
|
const decision = decide(options, startedByUs, protocol);
|
|
@@ -110,7 +127,14 @@ async function ensureHostLocked(paths, socket, options) {
|
|
|
110
127
|
case "refuse":
|
|
111
128
|
throw new HostEnsureRefusedError(socket, decision.reason, protocol);
|
|
112
129
|
case "handoff":
|
|
113
|
-
|
|
130
|
+
try {
|
|
131
|
+
return await upgradeGeneration(paths, socket, options, attachedPid);
|
|
132
|
+
}
|
|
133
|
+
catch (cause) {
|
|
134
|
+
if (cause instanceof HostEnsureRefusedError && cause.reason === "host_stopping")
|
|
135
|
+
return ensureHostLocked(paths, socket, options, stopDeadline);
|
|
136
|
+
throw cause;
|
|
137
|
+
}
|
|
114
138
|
case "start":
|
|
115
139
|
break;
|
|
116
140
|
default:
|
|
@@ -198,6 +222,10 @@ async function holdEnsured(socket) {
|
|
|
198
222
|
const held = await holdProtocolInfo(socket, EXISTING_HOST_PROBE_TIMEOUT_MS);
|
|
199
223
|
if (!held)
|
|
200
224
|
throw new Error(`RPC socket host at ${socket} stopped answering before this ensure could hold it`);
|
|
225
|
+
if (held.info.lifecycle === "stopping") {
|
|
226
|
+
held.hold.release();
|
|
227
|
+
throw new HostEnsureRefusedError(socket, "host_stopping", held.info);
|
|
228
|
+
}
|
|
201
229
|
return held.hold.release;
|
|
202
230
|
}
|
|
203
231
|
/** Whether a registration is about this endpoint. A record written before the field existed is. */
|
|
@@ -11,7 +11,8 @@ export declare class SupervisorDrain {
|
|
|
11
11
|
private graceTimer;
|
|
12
12
|
private readonly child;
|
|
13
13
|
private readonly shuttingDown;
|
|
14
|
-
|
|
14
|
+
private readonly beginStopping;
|
|
15
|
+
constructor(child: ChildProcess, shuttingDown: () => boolean, beginStopping?: () => void);
|
|
15
16
|
get active(): boolean;
|
|
16
17
|
/**
|
|
17
18
|
* Ask the child to announce and park attached sessions as turns/requests settle. The supervisor
|
|
@@ -3,10 +3,11 @@ import { DEFAULT_HANDOFF_GRACE_MS, HANDOFF_GRACE_MS_ENV, parseIdleExitMs } from
|
|
|
3
3
|
import { watchForSupersession } from "./host-supersession.js";
|
|
4
4
|
import { errorMessage, supervisorLog } from "./host-supervisor-log.js";
|
|
5
5
|
export class SupervisorDrain {
|
|
6
|
-
constructor(child, shuttingDown) {
|
|
6
|
+
constructor(child, shuttingDown, beginStopping = () => { }) {
|
|
7
7
|
this.draining = false;
|
|
8
8
|
this.child = child;
|
|
9
9
|
this.shuttingDown = shuttingDown;
|
|
10
|
+
this.beginStopping = beginStopping;
|
|
10
11
|
}
|
|
11
12
|
get active() {
|
|
12
13
|
return this.draining;
|
|
@@ -20,6 +21,7 @@ export class SupervisorDrain {
|
|
|
20
21
|
if (this.draining || this.shuttingDown())
|
|
21
22
|
return;
|
|
22
23
|
this.draining = true;
|
|
24
|
+
this.beginStopping();
|
|
23
25
|
supervisorLog("draining into the next generation");
|
|
24
26
|
const graceMs = parseIdleExitMs(process.env[HANDOFF_GRACE_MS_ENV]) ?? DEFAULT_HANDOFF_GRACE_MS;
|
|
25
27
|
this.graceTimer = setTimeout(() => {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Server } from "node:net";
|
|
2
|
+
import type { HostAdmission } from "./host-admission.ts";
|
|
2
3
|
import type { ClientOccupancy } from "./host-client-occupancy.ts";
|
|
3
4
|
import { type SocketFileIdentity } from "./socket-ownership.ts";
|
|
4
5
|
export interface PublicProxyOptions {
|
|
@@ -6,8 +7,8 @@ export interface PublicProxyOptions {
|
|
|
6
7
|
readonly internalSecret?: Buffer;
|
|
7
8
|
readonly publicSecret?: Buffer;
|
|
8
9
|
readonly clients: ClientOccupancy;
|
|
9
|
-
/**
|
|
10
|
-
readonly
|
|
10
|
+
/** Shared authority for stopping, protocol replies and new-session admission. */
|
|
11
|
+
readonly admission: HostAdmission;
|
|
11
12
|
/** A client went away: occupancy changed. */
|
|
12
13
|
readonly onDetach: () => void;
|
|
13
14
|
/** A dropped unclassified admission breaks quiescence; a proven observing read does not. */
|
|
@@ -6,46 +6,37 @@
|
|
|
6
6
|
import { access, chmod, mkdir, rename, unlink } from "node:fs/promises";
|
|
7
7
|
import { createConnection, createServer } from "node:net";
|
|
8
8
|
import { dirname } from "node:path";
|
|
9
|
+
import { proxyWithAdmission } from "./host-admission-proxy.js";
|
|
9
10
|
import { MAX_SOCKET_PATH_BYTES, statSocketIdentity } from "./socket-ownership.js";
|
|
10
11
|
import { authenticateSocket, ensureSocketSecret, resolveSocketTransportAddress, sendSocketHandshake, socketSecretPath, } from "./socket-transport.js";
|
|
11
12
|
export function createPublicProxy(options) {
|
|
12
13
|
const { internalSocket, internalSecret, publicSecret, clients } = options;
|
|
13
14
|
return createServer((client) => {
|
|
14
15
|
const accept = () => {
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
// that raced the rename, and a drain-stop with no successor at all.
|
|
18
|
-
if (options.refusing()) {
|
|
19
|
-
client.destroy();
|
|
16
|
+
// Teardown is terminal even for an accept that raced socket shielding/server.close.
|
|
17
|
+
if (!clients.admit(client))
|
|
20
18
|
return;
|
|
21
|
-
|
|
22
|
-
const internal =
|
|
23
|
-
|
|
19
|
+
// Draining peers can still probe and receive typed open refusals, without a child hop.
|
|
20
|
+
const internal = options.admission.stopping
|
|
21
|
+
? undefined
|
|
22
|
+
: createConnection(resolveSocketTransportAddress(internalSocket, process.platform, internalSecret));
|
|
23
|
+
if (internal && internalSecret)
|
|
24
24
|
sendSocketHandshake(internal, internalSecret);
|
|
25
25
|
// A readiness exchange can begin and end between ticks: the client is recorded the
|
|
26
26
|
// moment its first request line arrives, before a later tick can reuse the preceding
|
|
27
27
|
// idle window. An observing read (`status`) is never recorded (host-client-occupancy.ts).
|
|
28
|
-
clients.admit(client);
|
|
29
28
|
const detach = () => {
|
|
30
29
|
// Classification resets activity for attachments. Defer the unknown-admission reset
|
|
31
30
|
// until release so a status read never resets owner grace, even between ticks.
|
|
32
31
|
if (clients.release(client))
|
|
33
32
|
options.onAdmit?.();
|
|
34
33
|
options.onDetach();
|
|
35
|
-
internal
|
|
34
|
+
internal?.destroy();
|
|
36
35
|
client.destroy();
|
|
37
36
|
};
|
|
38
|
-
client.
|
|
39
|
-
internal.pipe(client);
|
|
37
|
+
proxyWithAdmission(client, internal, options.admission);
|
|
40
38
|
client.once("close", detach);
|
|
41
39
|
client.once("error", detach);
|
|
42
|
-
// Let the final lifecycle records drain through the public socket before closing it.
|
|
43
|
-
internal.once("end", () => client.end(() => client.destroy()));
|
|
44
|
-
internal.once("close", () => {
|
|
45
|
-
if (!internal.readableEnded)
|
|
46
|
-
detach();
|
|
47
|
-
});
|
|
48
|
-
internal.once("error", detach);
|
|
49
40
|
};
|
|
50
41
|
if (publicSecret)
|
|
51
42
|
authenticateSocket(client, publicSecret, accept);
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { ChildProcess } from "node:child_process";
|
|
7
7
|
import type { Server } from "node:net";
|
|
8
|
+
import type { HostAdmission } from "./host-admission.ts";
|
|
8
9
|
import type { HostStopSender } from "./host-crash-record.ts";
|
|
9
10
|
import type { HostDaemonPaths, HostGenerationPaths } from "./host-daemon-paths.ts";
|
|
10
11
|
import type { SupervisorActivity } from "./host-lifecycle-activity.ts";
|
|
@@ -31,6 +32,7 @@ export interface SupervisorShutdown {
|
|
|
31
32
|
readonly child: ChildProcess;
|
|
32
33
|
readonly activity: SupervisorActivity;
|
|
33
34
|
readonly drain: SupervisorDrain;
|
|
35
|
+
readonly admission: HostAdmission;
|
|
34
36
|
readonly state: SupervisorState;
|
|
35
37
|
/** Stops the idle ticker, the supersession watch and the win32 identity watchdog. */
|
|
36
38
|
readonly stopWatchers: () => void;
|
|
@@ -18,17 +18,19 @@ export async function performShutdown(context, reason, exitCode) {
|
|
|
18
18
|
const { state, child } = context;
|
|
19
19
|
state.shuttingDown = true;
|
|
20
20
|
state.shutdownReason = reason;
|
|
21
|
+
context.admission.stop();
|
|
21
22
|
let childStopStarted = false;
|
|
22
23
|
let hardExit;
|
|
23
24
|
try {
|
|
24
25
|
context.stopWatchers();
|
|
25
26
|
context.drain.stop();
|
|
26
27
|
writeStderrLine(`senpi rpc host supervisor: ${reason} shutdown`);
|
|
27
|
-
context.activity.clients.destroyAll();
|
|
28
28
|
// No fallible socket/metadata cleanup may bypass the child's observed exit.
|
|
29
29
|
childStopStarted = true;
|
|
30
30
|
await stopChild(context, reason);
|
|
31
31
|
await state.childExitRecorded;
|
|
32
|
+
await context.admission.flush();
|
|
33
|
+
context.activity.clients.destroyAll();
|
|
32
34
|
if (process.platform === "win32")
|
|
33
35
|
hardExit = setTimeout(() => process.exit(exitCode), WINDOWS_SUPERVISOR_SHUTDOWN_HARD_EXIT_MS);
|
|
34
36
|
// libuv unlinks the bound NAME when the listening handle closes - which would delete a newer
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { SupervisorActivity, SupervisorOwner } from "./host-lifecycle-activity.ts";
|
|
2
|
+
/** Idle and lifetime-owner checks share one ticker and never overlap owner probes. */
|
|
3
|
+
export declare function startSupervisorTicker(options: {
|
|
4
|
+
readonly idleExitMs: number;
|
|
5
|
+
readonly activity: SupervisorActivity;
|
|
6
|
+
readonly owner: SupervisorOwner;
|
|
7
|
+
readonly draining: () => boolean;
|
|
8
|
+
readonly shuttingDown: () => boolean;
|
|
9
|
+
readonly shutdown: (reason: string, exitCode: number) => Promise<never>;
|
|
10
|
+
}): () => void;
|
|
11
|
+
//# sourceMappingURL=host-lifecycle-ticker.d.ts.map
|