@nathapp/nax-agent 0.91.7 → 0.91.8-canary.2
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 +8 -0
- package/dist/config/catalog-overrides.d.ts +13 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -0
- package/dist/internal/atomic-write.d.ts +22 -7
- package/dist/internal/atomic-write.js +107 -12
- package/dist/internal/pinned-directory-worker.d.ts +52 -0
- package/dist/internal/pinned-directory-worker.js +105 -0
- package/dist/internal/pinned-directory.d.ts +48 -0
- package/dist/internal/pinned-directory.js +168 -0
- package/dist/internal.d.ts +2 -0
- package/dist/internal.js +2 -0
- package/dist/memory/delivery.d.ts +38 -0
- package/dist/memory/delivery.js +28 -0
- package/dist/memory/index-render.d.ts +55 -0
- package/dist/memory/index-render.js +225 -0
- package/dist/memory/index.d.ts +12 -0
- package/dist/memory/index.js +11 -0
- package/dist/memory/local-provider.d.ts +3 -0
- package/dist/memory/local-provider.js +127 -0
- package/dist/memory/mirror-lock.d.ts +109 -0
- package/dist/memory/mirror-lock.js +334 -0
- package/dist/memory/note.d.ts +15 -0
- package/dist/memory/note.js +131 -0
- package/dist/memory/operations.d.ts +123 -0
- package/dist/memory/operations.js +471 -0
- package/dist/memory/outbox.d.ts +35 -0
- package/dist/memory/outbox.js +289 -0
- package/dist/memory/provider-types.d.ts +50 -0
- package/dist/memory/provider-types.js +1 -0
- package/dist/memory/records.d.ts +41 -0
- package/dist/memory/records.js +93 -0
- package/dist/memory/refusal.d.ts +36 -0
- package/dist/memory/refusal.js +25 -0
- package/dist/memory/search.d.ts +17 -0
- package/dist/memory/search.js +112 -0
- package/dist/memory/store.d.ts +106 -0
- package/dist/memory/store.js +459 -0
- package/dist/memory/support.d.ts +63 -0
- package/dist/memory/support.js +98 -0
- package/dist/memory/sync-flight.d.ts +25 -0
- package/dist/memory/sync-flight.js +65 -0
- package/dist/memory/sync.d.ts +82 -0
- package/dist/memory/sync.js +344 -0
- package/dist/memory/types.d.ts +108 -0
- package/dist/memory/types.js +51 -0
- package/dist/memory/validate.d.ts +31 -0
- package/dist/memory/validate.js +104 -0
- package/dist/native/adapter-deps.d.ts +9 -0
- package/dist/native/adapter-deps.js +9 -0
- package/dist/native/models.js +3 -0
- package/dist/native/session/compaction.d.ts +15 -1
- package/dist/native/session/compaction.js +90 -23
- package/dist/native/session/manual-compaction.d.ts +57 -0
- package/dist/native/session/manual-compaction.js +72 -0
- package/dist/native/session/session-summarizer.d.ts +75 -0
- package/dist/native/session/session-summarizer.js +73 -0
- package/dist/native/session/transcript-identity.d.ts +12 -0
- package/dist/native/session/transcript-identity.js +16 -0
- package/dist/native/session/turn-compaction-step.d.ts +30 -2
- package/dist/native/session/turn-compaction-step.js +7 -1
- package/dist/native/session/turn-loop-round-trip.js +71 -17
- package/dist/native/session/turn-loop.js +2 -6
- package/dist/native/session-adapter.d.ts +19 -0
- package/dist/native/session-adapter.js +42 -64
- package/dist/sandbox/srt-backend.js +9 -16
- package/dist/session/agent-session-options.js +1 -0
- package/dist/session/agent-session-turn.d.ts +6 -0
- package/dist/session/agent-session-turn.js +5 -4
- package/dist/session/agent-session-types.d.ts +38 -0
- package/dist/session/agent-session.d.ts +8 -0
- package/dist/session/agent-session.js +163 -20
- package/dist/session/native-backend.js +8 -0
- package/dist/session/session-backend.d.ts +19 -1
- package/dist/session/session-types.d.ts +6 -0
- package/dist/session/steer-channel.d.ts +36 -0
- package/dist/session/steer-channel.js +39 -0
- package/dist/session-store/index.d.ts +1 -1
- package/dist/session-store/index.js +1 -1
- package/dist/session-store/session-lock.d.ts +2 -2
- package/dist/session-store/session-lock.js +4 -2
- package/dist/tools/ask-request.d.ts +8 -0
- package/dist/tools/ask-request.js +34 -1
- package/dist/tools/provider-adapt.js +5 -1
- package/dist/tools/provider-types.d.ts +7 -0
- package/dist/tools/types.d.ts +16 -0
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,14 @@ All notable changes to `@nathapp/nax-agent` are recorded here. The format follow
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/). While the version is `0.x`, a minor
|
|
5
5
|
release may change the public API.
|
|
6
6
|
|
|
7
|
+
## [0.91.8-canary.2] - 2026-11-04
|
|
8
|
+
|
|
9
|
+
- No changes in this package. Released in lockstep with the other nax packages.
|
|
10
|
+
|
|
11
|
+
## [0.91.8-canary.1] - 2026-11-03
|
|
12
|
+
|
|
13
|
+
- No changes in this package. Released in lockstep with the other nax packages.
|
|
14
|
+
|
|
7
15
|
## [0.91.7] - 2026-11-01
|
|
8
16
|
|
|
9
17
|
- No changes in this package. Released in lockstep with the other nax packages.
|
|
@@ -65,6 +65,15 @@ export interface OpenRouterRouting {
|
|
|
65
65
|
p99?: number;
|
|
66
66
|
};
|
|
67
67
|
}
|
|
68
|
+
/** Per-level token budgets, hand-mirrored from nax-ai's ThinkingBudgets.
|
|
69
|
+
* Config code cannot import nax-ai; validation remains owned by nax-ai.
|
|
70
|
+
*/
|
|
71
|
+
export interface CatalogThinkingBudgets {
|
|
72
|
+
minimal?: number;
|
|
73
|
+
low?: number;
|
|
74
|
+
medium?: number;
|
|
75
|
+
high?: number;
|
|
76
|
+
}
|
|
68
77
|
/**
|
|
69
78
|
* One complete catalog entry for a model the bundled pi-ai snapshot does not
|
|
70
79
|
* know. Complete, not a patch: nax-ai's `normaliseCatalog` replaces any
|
|
@@ -98,6 +107,10 @@ export interface CatalogModelOverride {
|
|
|
98
107
|
* Mirrored from nax-ai's ThinkingFormat; omit to inherit the provider's format.
|
|
99
108
|
*/
|
|
100
109
|
thinkingFormat?: "openai" | "qwen" | "qwen-chat-template" | "deepseek" | "zai" | "openrouter";
|
|
110
|
+
/** Wire field receiving the thinking token budget on openai-completions models. */
|
|
111
|
+
thinkingBudgetField?: string;
|
|
112
|
+
/** Model-owned token budgets for declared reasoning levels. */
|
|
113
|
+
thinkingBudgets?: CatalogThinkingBudgets;
|
|
101
114
|
}
|
|
102
115
|
/** Provider-scoped catalog overrides — maps 1:1 onto nax-ai's `ProviderOverride[]`. */
|
|
103
116
|
export interface ProviderCatalogOverride {
|
package/dist/index.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ export { isProcessAlive } from "#src/internal/process-alive";
|
|
|
19
19
|
export { killProcessGroup } from "#src/internal/process-kill";
|
|
20
20
|
export { capStrings, redactSecrets } from "#src/internal/redact";
|
|
21
21
|
export { NaxError } from "#src/infra/nax-error";
|
|
22
|
+
export { MEMORY_CLIENT_ID_RE, MEMORY_DESCRIPTION_MAX_CHARS, MEMORY_FORGET_REASON_MAX_CHARS, MEMORY_INDEX_MAX_BYTES, MEMORY_INDEX_MAX_LINES, MEMORY_INDEX_MIN_BYTES, MEMORY_INDEX_MIN_LINES, MEMORY_NOTE_BODY_MAX_BYTES, MEMORY_OP_TIMEOUT_MS, MEMORY_PROBE_TIMEOUT_MS, MEMORY_PULL_MAX_PAGES, MEMORY_PULL_PAGE_SIZE, MEMORY_SLUG_RE, MEMORY_SOURCE_MAX_CHARS, type MemoryApproveInput, type MemoryDecision, type MemoryForgetInput, type MemoryForgetResult, type MemoryHit, type MemoryNote, type MemoryNoteStatus, type MemoryNoteType, type MemoryNoteWrite, type MemoryOutbox, type MemoryOutboxEntry, type MemoryPage, type MemoryPendingForget, type MemoryProvider, type MemoryPushResult, type MemorySaveInput, type MemorySaveResult, type MemoryScope, type MemoryStore, type MemoryStorePaths, type MemorySyncResult, type MemorySyncState, type MemoryTimers, type MemoryWriteRefusal, type MemoryWriteRequest, type RenderedMemoryIndex, type RefreshMemoryIndexOptions, type RenderMemoryIndexInput, approveMemoryNote, createLocalMemoryProvider, createMemoryOutbox, createMemoryStore, findSecret, forgetMemoryNote, isMemoryPushRejected, parseMemoryNote, raceTimeout, refreshMemoryIndex, renderMemoryIndex, saveMemoryNote, searchMemoryNotes, serializeMemoryNote, syncMemory, tokenizeMemoryQuery, validateMemoryNoteWrite, } from "#src/memory/index";
|
|
22
23
|
export { createTerminalAuthInteraction, PLAIN_STYLE, PromptCancelledError, type TerminalAuthOptions, type TerminalStyle, } from "#src/terminal-auth/index";
|
|
23
24
|
export type { CredentialSource } from "#src/native/credentials/session-source";
|
|
24
25
|
export { AuthCancelledError, type AuthEvent, type AuthInteraction, type AuthLink, type AuthMethod, type AuthOption, type AuthPrompt, type AuthResult, ambientShadows, anyAmbientCredential, authImportOutcomeLabel, createFileTranscriptStore, createMemoryTranscriptStore, credentialFilePath, DEFAULT_PI_AUTH_PATH, type ImportOutcome, importPiCredentials, listProviderModels, listStoredProviders, loginProviderIds, MAX_RETAINED_TRANSCRIPTS, type MemoryTranscriptStore, NATIVE_AGENT, type NativeCatalogOverrides, type NativeCompleteContext, type NativeCompleteOptions, type NativeCompleteResult, NativeSessionAdapter, NativeSessionUnsupportedError, type NativeTierConfig, nativeComplete, nativeTierProviders, naxCredentialStore, newSessionKey, type ProviderModel, providersWithoutCredentials, pruneRetainedTranscripts, type ResolveResult, type ResolveStatus, removeStoredProvider, resolveNativeId, runLogin, type StoredEntry, servedAuth, type TranscriptAcpRecord, type TranscriptDoc, type TranscriptStore, type TurnMarker, } from "#src/native/index";
|
|
@@ -32,7 +33,7 @@ export { createSessionStorage, defaultSessionsDir, processAlive, LIST_PAGE_SIZE,
|
|
|
32
33
|
export type { AdapterFailure } from "#src/session/adapter-failure";
|
|
33
34
|
export { createAgentSession, resumeAgentSession } from "#src/session/agent-session";
|
|
34
35
|
export { AgentSessionError, type AgentSessionErrorCode } from "#src/session/agent-session-errors";
|
|
35
|
-
export type { AgentSession, AgentSessionHostPorts, AgentSessionProfile, AnswerReply, AnswerStatus, ApprovalDecidedBy, CostSource, CommandRunResult, CreateAgentSessionOptions, EmbedderTool, EmbedderToolContext, EmbedderToolResult, SessionConfigureInput, SessionEvent, SessionEventBase, SessionEventBody, TurnEndStatus, } from "#src/session/agent-session-types";
|
|
36
|
+
export type { AgentSession, AgentSessionHostPorts, AgentSessionProfile, AnswerReply, AnswerStatus, ApprovalDecidedBy, CostSource, CommandRunResult, CompactResult, CreateAgentSessionOptions, EmbedderTool, EmbedderToolContext, EmbedderToolResult, SessionConfigureInput, SessionEvent, SessionEventBase, SessionEventBody, TurnEndStatus, } from "#src/session/agent-session-types";
|
|
36
37
|
export type { AgentAwaitingHumanEvent, AgentCallEndedEvent, AgentCallStartedEvent, AgentMessageUpdateEvent, AgentProcessUpdateEvent, AgentStreamEvent, AgentStreamEventBase, AgentThinkingUpdateEvent, AgentToolCallUpdateEvent, AgentUsageUpdateEvent, } from "#src/session/agent-stream-events";
|
|
37
38
|
export { IMAGE_MIME_TYPES, IMAGE_TOKEN_ESTIMATE, MAX_IMAGE_BASE64_CHARS, MAX_IMAGES_PER_MESSAGE, MAX_REQUEST_IMAGE_BASE64_CHARS, MAX_REQUEST_IMAGES, type SendInput, validateImageAttachments, } from "#src/session/image-input";
|
|
38
39
|
export { type AdapterInteraction, type AdapterInteractionResponse, type InteractionHandler, NO_OP_INTERACTION_HANDLER, } from "#src/session/interaction-handler";
|
|
@@ -41,6 +42,7 @@ export type { NativeBackendOptions } from "#src/session/native-backend-options";
|
|
|
41
42
|
export type { ProtocolIds } from "#src/session/protocol-types";
|
|
42
43
|
export type { ApprovalRequest, BackendInfo, BackendOpenContext, OpenedBackend, SessionAskPort, SessionBackend, TurnContribution, } from "#src/session/session-backend";
|
|
43
44
|
export { type AgentSessionAdapter, type AuthStamp, type InteractionExchange, type InvalidToolCallDetail, type OpenSessionOpts, type SendTurnOpts, type SessionHandle, type SessionModel, SessionTurnError, type TrackedSpawnDeadlineOptions, type TurnResult, } from "#src/session/session-types";
|
|
45
|
+
export type { SteerChannel, SteerResult } from "#src/session/steer-channel";
|
|
44
46
|
export { attachTurnSpend, type FailedTurnSpend, readTurnSpend } from "#src/session/turn-spend";
|
|
45
47
|
export type { JSONSchema, ToolDescriptor, } from "#src/session/tool-descriptor";
|
|
46
48
|
export { createTurnDeadline, type TurnDeadline, } from "#src/session/turn-deadline";
|
package/dist/index.js
CHANGED
|
@@ -16,6 +16,7 @@ export { isProcessAlive } from "#src/internal/process-alive";
|
|
|
16
16
|
export { killProcessGroup } from "#src/internal/process-kill";
|
|
17
17
|
export { capStrings, redactSecrets } from "#src/internal/redact";
|
|
18
18
|
export { NaxError } from "#src/infra/nax-error";
|
|
19
|
+
export { MEMORY_CLIENT_ID_RE, MEMORY_DESCRIPTION_MAX_CHARS, MEMORY_FORGET_REASON_MAX_CHARS, MEMORY_INDEX_MAX_BYTES, MEMORY_INDEX_MAX_LINES, MEMORY_INDEX_MIN_BYTES, MEMORY_INDEX_MIN_LINES, MEMORY_NOTE_BODY_MAX_BYTES, MEMORY_OP_TIMEOUT_MS, MEMORY_PROBE_TIMEOUT_MS, MEMORY_PULL_MAX_PAGES, MEMORY_PULL_PAGE_SIZE, MEMORY_SLUG_RE, MEMORY_SOURCE_MAX_CHARS, approveMemoryNote, createLocalMemoryProvider, createMemoryOutbox, createMemoryStore, findSecret, forgetMemoryNote, isMemoryPushRejected, parseMemoryNote, raceTimeout, refreshMemoryIndex, renderMemoryIndex, saveMemoryNote, searchMemoryNotes, serializeMemoryNote, syncMemory, tokenizeMemoryQuery, validateMemoryNoteWrite, } from "#src/memory/index";
|
|
19
20
|
export { createTerminalAuthInteraction, PLAIN_STYLE, PromptCancelledError, } from "#src/terminal-auth/index";
|
|
20
21
|
export { AuthCancelledError, ambientShadows, anyAmbientCredential, authImportOutcomeLabel, createFileTranscriptStore, createMemoryTranscriptStore, credentialFilePath, DEFAULT_PI_AUTH_PATH, importPiCredentials, listProviderModels, listStoredProviders, loginProviderIds, MAX_RETAINED_TRANSCRIPTS, NATIVE_AGENT, NativeSessionAdapter, NativeSessionUnsupportedError, nativeComplete, nativeTierProviders, naxCredentialStore, newSessionKey, providersWithoutCredentials, pruneRetainedTranscripts, removeStoredProvider, resolveNativeId, runLogin, servedAuth, } from "#src/native/index";
|
|
21
22
|
export { displayToolInput, TOOL_CALL_INPUT_BYTES, TOOL_RESULT_PREVIEW_BYTES, toolResultPreview, } from "#src/native/session/turn-event-emitter";
|
|
@@ -1,16 +1,31 @@
|
|
|
1
1
|
import { rename, rm } from "node:fs/promises";
|
|
2
|
-
/**
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
}
|
|
7
|
-
/**
|
|
2
|
+
/** What a file is, independent of the path that names it. */
|
|
3
|
+
interface Identity {
|
|
4
|
+
readonly dev: number;
|
|
5
|
+
readonly ino: number;
|
|
6
|
+
}
|
|
7
|
+
/** One created stage: written through its descriptor, identified by it, then closed. */
|
|
8
|
+
interface StagedFile {
|
|
9
|
+
write(content: string | Uint8Array): Promise<void>;
|
|
10
|
+
identity(): Promise<Identity>;
|
|
11
|
+
close(): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
/** Try removing an empty stage directory, never its contents. Injectable for tests. */
|
|
14
|
+
type DiscardStage = (stageDir: string) => Promise<void>;
|
|
15
|
+
/** Injectable for tests that simulate a write dying part-way, or a stage namespace being taken. */
|
|
8
16
|
export declare const _atomicWriteDeps: {
|
|
9
|
-
|
|
17
|
+
stage: (dir: string, mode: number) => Promise<void>;
|
|
18
|
+
open: (path: string, flags: number, mode: number) => Promise<StagedFile>;
|
|
10
19
|
rename: typeof rename;
|
|
11
20
|
rm: typeof rm;
|
|
21
|
+
discard: DiscardStage;
|
|
12
22
|
};
|
|
13
23
|
export declare function writeFileAtomic(path: string, content: string | Uint8Array, options?: {
|
|
14
24
|
mode?: number;
|
|
25
|
+
beforePublish?: () => Promise<void>;
|
|
26
|
+
afterPublish?: (stage: {
|
|
27
|
+
dev: number;
|
|
28
|
+
ino: number;
|
|
29
|
+
}) => Promise<void>;
|
|
15
30
|
}): Promise<void>;
|
|
16
31
|
export {};
|
|
@@ -8,27 +8,122 @@
|
|
|
8
8
|
* sees the old file or the new one. Same-directory staging keeps the rename on
|
|
9
9
|
* one filesystem, where POSIX makes it atomic. Not fsync'd: this guards against
|
|
10
10
|
* a killed PROCESS, not a power loss.
|
|
11
|
+
*
|
|
12
|
+
* The stage is a *directory* (`<path>.<uuid>.tmp`) created exclusively by this
|
|
13
|
+
* call. Exclusive creation and mode 0700 do not establish continuing ownership:
|
|
14
|
+
* a writer of its parent can rename it and put a foreign directory or link at
|
|
15
|
+
* its name. Cleanup therefore NEVER unlinks a payload through that pathname.
|
|
16
|
+
* It only attempts `rmdir`, which neither follows a final symlink nor removes a
|
|
17
|
+
* nonempty directory. Failed writes conservatively retain their payloads; even
|
|
18
|
+
* a replacement between the last check and cleanup cannot lose foreign bytes.
|
|
19
|
+
* Successful publication normally leaves an empty stage directory to remove.
|
|
20
|
+
* A retained `.tmp` namespace is ignored by the package's listings. No pathname
|
|
21
|
+
* identity check is claimed to make cleanup ownership-atomic.
|
|
22
|
+
*
|
|
23
|
+
* In a pinned-directory context all stage, publication and cleanup operations
|
|
24
|
+
* are relative to the verified cwd, and the stage directory is reached as one
|
|
25
|
+
* deeper operand of it. Ordinary callers retain path-based I/O. The hooks
|
|
26
|
+
* additionally verify the destination:
|
|
27
|
+
*
|
|
28
|
+
* - `beforePublish` runs once the bytes are staged and before the rename, so a
|
|
29
|
+
* destination that moved during the write is refused before it receives them;
|
|
30
|
+
* - `afterPublish` receives the staged file's identity once the rename is done, so a
|
|
31
|
+
* caller can check — and take back by identity — a note that landed somewhere the
|
|
32
|
+
* rename could not be told apart from.
|
|
33
|
+
*
|
|
34
|
+
* A throw from either hook fails the write; cleanup retains any remaining payload.
|
|
11
35
|
*/
|
|
12
36
|
import { randomUUID } from "node:crypto";
|
|
13
|
-
import {
|
|
14
|
-
|
|
37
|
+
import { constants } from "node:fs";
|
|
38
|
+
import { mkdir, open, rename, rm, rmdir } from "node:fs/promises";
|
|
39
|
+
import { join } from "node:path";
|
|
40
|
+
import { openPinnedStage, pinnedOperation } from "./pinned-directory.js";
|
|
41
|
+
/** Exclusive creation, and never through a link at the stage's name. */
|
|
42
|
+
const STAGE_FLAGS = constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW;
|
|
43
|
+
/** Restrict access inside the stage, without claiming that its parent cannot replace it. */
|
|
44
|
+
const STAGE_DIR_MODE = 0o700;
|
|
45
|
+
/** The default a plain `writeFile` would apply, so a published note keeps ordinary permissions. */
|
|
46
|
+
const STAGE_MODE = 0o666;
|
|
47
|
+
/** The suffix every stage directory carries; the package's listings ignore those names. */
|
|
48
|
+
const STAGE_SUFFIX = ".tmp";
|
|
49
|
+
/** The payload's name inside the stage directory: the only entry this call ever puts there. */
|
|
50
|
+
const STAGE_NAME = "stage";
|
|
51
|
+
/** Injectable for tests that simulate a write dying part-way, or a stage namespace being taken. */
|
|
15
52
|
export const _atomicWriteDeps = {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
53
|
+
stage: async (dir, mode) => {
|
|
54
|
+
const pinned = await pinnedOperation(dir, "stage", [mode]);
|
|
55
|
+
if (pinned !== null)
|
|
56
|
+
return await pinned;
|
|
57
|
+
await mkdir(dir, { mode });
|
|
58
|
+
},
|
|
59
|
+
open: async (path, flags, mode) => {
|
|
60
|
+
const pinned = await openPinnedStage(path, flags, mode);
|
|
61
|
+
if (pinned)
|
|
62
|
+
return pinned;
|
|
63
|
+
const handle = await open(path, flags, mode);
|
|
64
|
+
return {
|
|
65
|
+
write: (content) => handle.writeFile(content, { encoding: "utf8" }),
|
|
66
|
+
identity: async () => {
|
|
67
|
+
const info = await handle.stat();
|
|
68
|
+
return { dev: info.dev, ino: info.ino };
|
|
69
|
+
},
|
|
70
|
+
close: () => handle.close(),
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
rename: (from, to) => pinnedOperation(String(from), "rename", [String(to)]) ?? rename(from, to),
|
|
74
|
+
rm: (path, options) => pinnedOperation(String(path), "rm") ?? rm(path, options),
|
|
75
|
+
discard: (dir) => removeStage(dir),
|
|
19
76
|
};
|
|
77
|
+
/** Remove only an empty namespace; a payload's ownership cannot be proved by its pathname. */
|
|
78
|
+
async function removeStage(stageDir) {
|
|
79
|
+
const pinned = pinnedOperation(stageDir, "discard");
|
|
80
|
+
if (pinned !== null)
|
|
81
|
+
return await pinned;
|
|
82
|
+
await rmdir(stageDir);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Remove the stage this call created, or leave it in place when it cannot be emptied.
|
|
86
|
+
*
|
|
87
|
+
* A removal that fails is not reported: the failure the caller is owed is the one that
|
|
88
|
+
* brought us here, and a stage left behind is a name every listing in the package ignores.
|
|
89
|
+
*/
|
|
90
|
+
async function discardStage(stageDir, owned) {
|
|
91
|
+
if (!owned)
|
|
92
|
+
return;
|
|
93
|
+
await _atomicWriteDeps.discard(stageDir).catch(() => {
|
|
94
|
+
// A stage that could not be emptied is retained; the pinned worker reports its own errors.
|
|
95
|
+
});
|
|
96
|
+
}
|
|
20
97
|
export async function writeFileAtomic(path, content, options = {}) {
|
|
21
98
|
// Ends in `.tmp`, not `.json`, so directory sweeps that match `*.json` never see it.
|
|
22
|
-
const staged = `${path}.${randomUUID()}
|
|
99
|
+
const staged = `${path}.${randomUUID()}${STAGE_SUFFIX}`;
|
|
100
|
+
const payload = join(staged, STAGE_NAME);
|
|
101
|
+
let stage = null;
|
|
102
|
+
let owned = false;
|
|
103
|
+
let identity = null;
|
|
23
104
|
try {
|
|
24
|
-
await _atomicWriteDeps.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
105
|
+
await _atomicWriteDeps.stage(staged, STAGE_DIR_MODE);
|
|
106
|
+
// Only a namespace this call created is this call's to remove.
|
|
107
|
+
owned = true;
|
|
108
|
+
stage = await _atomicWriteDeps.open(payload, STAGE_FLAGS, options.mode ?? STAGE_MODE);
|
|
109
|
+
// Capture ownership before writing: a failed write may already have lost its pathname.
|
|
110
|
+
identity = await stage.identity();
|
|
111
|
+
await stage.write(content);
|
|
112
|
+
if (options.beforePublish)
|
|
113
|
+
await options.beforePublish();
|
|
114
|
+
await _atomicWriteDeps.rename(payload, path);
|
|
115
|
+
if (options.afterPublish && identity !== null)
|
|
116
|
+
await options.afterPublish(identity);
|
|
29
117
|
}
|
|
30
118
|
catch (error) {
|
|
31
|
-
await
|
|
119
|
+
await discardStage(staged, owned);
|
|
32
120
|
throw error;
|
|
33
121
|
}
|
|
122
|
+
finally {
|
|
123
|
+
await stage?.close().catch(() => {
|
|
124
|
+
// A stage whose write already failed cannot be rescued by reporting the close either.
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
// The published payload left the namespace empty: the directory is all that remains.
|
|
128
|
+
await discardStage(staged, owned);
|
|
34
129
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The filesystem surface the worker drives: one member per operation it serves.
|
|
3
|
+
*
|
|
4
|
+
* Named rather than `typeof import("node:fs/promises")` so a test can answer a single
|
|
5
|
+
* operation (the real module still satisfies it structurally, so the caller passes it as is).
|
|
6
|
+
*/
|
|
7
|
+
export interface PinnedWorkerFs {
|
|
8
|
+
stat(path: string): Promise<{
|
|
9
|
+
readonly dev: number;
|
|
10
|
+
readonly ino: number;
|
|
11
|
+
}>;
|
|
12
|
+
lstat(path: string): Promise<{
|
|
13
|
+
readonly dev: number;
|
|
14
|
+
readonly ino: number;
|
|
15
|
+
}>;
|
|
16
|
+
open(path: string, flags: number, mode: number): Promise<{
|
|
17
|
+
readFile(options: {
|
|
18
|
+
encoding: "utf8";
|
|
19
|
+
}): Promise<string>;
|
|
20
|
+
writeFile(content: Uint8Array): Promise<void>;
|
|
21
|
+
stat(): Promise<{
|
|
22
|
+
readonly dev: number;
|
|
23
|
+
readonly ino: number;
|
|
24
|
+
}>;
|
|
25
|
+
close(): Promise<void>;
|
|
26
|
+
}>;
|
|
27
|
+
realpath(path: string): Promise<string>;
|
|
28
|
+
mkdir(path: string, options: {
|
|
29
|
+
readonly mode: number;
|
|
30
|
+
}): Promise<string | undefined>;
|
|
31
|
+
rename(from: string, to: string): Promise<void>;
|
|
32
|
+
rm(path: string, options: {
|
|
33
|
+
readonly force: true;
|
|
34
|
+
}): Promise<void>;
|
|
35
|
+
unlink(path: string): Promise<void>;
|
|
36
|
+
rmdir(path: string): Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Runs in a dedicated process whose cwd pins a directory inode on POSIX, including
|
|
40
|
+
* macOS (where /dev/fd/<dirfd>/name is not traversable). Never chdir in the caller:
|
|
41
|
+
* its cwd is process-global. Keep this function self-contained for `node -e`.
|
|
42
|
+
*/
|
|
43
|
+
export declare function pinnedDirectoryWorker(fs: PinnedWorkerFs, io: {
|
|
44
|
+
input: NodeJS.ReadableStream;
|
|
45
|
+
write(text: string): unknown;
|
|
46
|
+
createInterface: typeof import("node:readline").createInterface;
|
|
47
|
+
/**
|
|
48
|
+
* An absolute path that names this worker's cwd *by inode* (Linux: `/proc/self/cwd`),
|
|
49
|
+
* or null where the platform has none. See `anchored` below for what it buys.
|
|
50
|
+
*/
|
|
51
|
+
anchor?: string | null;
|
|
52
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runs in a dedicated process whose cwd pins a directory inode on POSIX, including
|
|
3
|
+
* macOS (where /dev/fd/<dirfd>/name is not traversable). Never chdir in the caller:
|
|
4
|
+
* its cwd is process-global. Keep this function self-contained for `node -e`.
|
|
5
|
+
*/
|
|
6
|
+
export async function pinnedDirectoryWorker(fs, io) {
|
|
7
|
+
/**
|
|
8
|
+
* Absolute spellings, for the one operation that cannot be spelled relatively.
|
|
9
|
+
*
|
|
10
|
+
* Every other operation hands the kernel a relative name, which it resolves against the
|
|
11
|
+
* cwd inode: renaming the directory afterwards cannot redirect `open`, `rename` or `stat`.
|
|
12
|
+
* `realpath` is the exception — it takes an absolute path, and a runtime that builds one
|
|
13
|
+
* from a *cached* cwd string (Bun on Linux caches the spelling, not the inode) hands the
|
|
14
|
+
* kernel the name the directory used to have, which a replacement can own. The anchor is
|
|
15
|
+
* a magic link the kernel resolves to the inode itself, so the answer still describes the
|
|
16
|
+
* directory this worker pinned.
|
|
17
|
+
*
|
|
18
|
+
* The anchor is checked against `.` before first use: an anchor the host does not have
|
|
19
|
+
* (no `/proc`, or a foreign one) is dropped and the bare name used instead.
|
|
20
|
+
*/
|
|
21
|
+
let anchor = io.anchor ?? null;
|
|
22
|
+
if (anchor !== null) {
|
|
23
|
+
try {
|
|
24
|
+
const [at, dot] = [await fs.stat(anchor), await fs.stat(".")];
|
|
25
|
+
if (at.dev !== dot.dev || at.ino !== dot.ino)
|
|
26
|
+
anchor = null;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// This host offers no such anchor; the relative name is all there is.
|
|
30
|
+
anchor = null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const anchored = (name) => (anchor === null ? name : `${anchor}/${name}`);
|
|
34
|
+
const files = new Map();
|
|
35
|
+
let next = 0;
|
|
36
|
+
const identity = (info) => ({ dev: info.dev, ino: info.ino });
|
|
37
|
+
const fileAt = (id) => {
|
|
38
|
+
const file = files.get(Number(id));
|
|
39
|
+
if (!file)
|
|
40
|
+
throw new Error(`Unknown pinned descriptor: ${id}`); // nax-lint-allow: plain-error — isolated wire protocol, reconstructed as NaxError by caller
|
|
41
|
+
return file;
|
|
42
|
+
};
|
|
43
|
+
const lines = io.createInterface({ input: io.input });
|
|
44
|
+
for await (const line of lines) {
|
|
45
|
+
try {
|
|
46
|
+
const { op, args } = JSON.parse(line);
|
|
47
|
+
let value;
|
|
48
|
+
switch (op) {
|
|
49
|
+
case "directory":
|
|
50
|
+
value = identity(await fs.stat("."));
|
|
51
|
+
break;
|
|
52
|
+
case "open": {
|
|
53
|
+
const handle = await fs.open(args[0], Number(args[1]), args[2]);
|
|
54
|
+
files.set(++next, handle);
|
|
55
|
+
value = next;
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
case "read":
|
|
59
|
+
value = await fileAt(args[0]).readFile({ encoding: "utf8" });
|
|
60
|
+
break;
|
|
61
|
+
case "write":
|
|
62
|
+
await fileAt(args[0]).writeFile(Buffer.from(args[1], "base64"));
|
|
63
|
+
break;
|
|
64
|
+
case "identity":
|
|
65
|
+
value = identity(await fileAt(args[0]).stat());
|
|
66
|
+
break;
|
|
67
|
+
case "resolve":
|
|
68
|
+
value = await fs.realpath(anchored(args[0]));
|
|
69
|
+
break;
|
|
70
|
+
case "stage":
|
|
71
|
+
await fs.mkdir(args[0], { mode: Number(args[1]) });
|
|
72
|
+
break;
|
|
73
|
+
case "close": {
|
|
74
|
+
await fileAt(args[0]).close();
|
|
75
|
+
files.delete(Number(args[0]));
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
case "stat":
|
|
79
|
+
value = identity(await fs.lstat(args[0]));
|
|
80
|
+
break;
|
|
81
|
+
case "rename":
|
|
82
|
+
await fs.rename(args[0], args[1]);
|
|
83
|
+
break;
|
|
84
|
+
case "rm":
|
|
85
|
+
await fs.rm(args[0], { force: true });
|
|
86
|
+
break;
|
|
87
|
+
case "discard": {
|
|
88
|
+
// The pinned parent does not pin a stage's own namespace: a writer of that
|
|
89
|
+
// parent can replace it. Never unlink through it. rmdir removes only empty
|
|
90
|
+
// directories and refuses final symlinks; nonempty stages are retained.
|
|
91
|
+
await fs.rmdir(args[0]);
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
default:
|
|
95
|
+
throw new Error(`Unknown pinned operation: ${op}`); // nax-lint-allow: plain-error — isolated wire protocol, reconstructed as NaxError by caller
|
|
96
|
+
}
|
|
97
|
+
io.write(`${JSON.stringify({ value })}\n`);
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
const failure = error;
|
|
101
|
+
io.write(`${JSON.stringify({ error: failure.message, code: failure.code })}\n`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
await Promise.all([...files.values()].map((file) => file.close()));
|
|
105
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
interface Identity {
|
|
2
|
+
readonly dev: number;
|
|
3
|
+
readonly ino: number;
|
|
4
|
+
}
|
|
5
|
+
/** Resolve one operand to a request: the same check for a single path and for a rename pair. */
|
|
6
|
+
export declare function pinnedOperation<T>(path: string, op: string, args?: unknown[]): Promise<T> | null;
|
|
7
|
+
/**
|
|
8
|
+
* Spawn first, then verify the child's cwd identity before sending any mutation.
|
|
9
|
+
* A swap during spawn therefore fails closed. Later swaps cannot redirect cwd-relative
|
|
10
|
+
* operations. AsyncLocalStorage keeps concurrent stores/seam wrappers isolated without
|
|
11
|
+
* changing the existing `_atomicWriteDeps` call signatures.
|
|
12
|
+
*/
|
|
13
|
+
export declare function withPinnedDirectory<T>(input: {
|
|
14
|
+
path: string;
|
|
15
|
+
identity: Identity;
|
|
16
|
+
changed: () => Error;
|
|
17
|
+
}, action: () => Promise<T>): Promise<T>;
|
|
18
|
+
/** Opened stage RPCs use the worker's descriptor table, never a staging pathname. */
|
|
19
|
+
export declare function openPinnedStage(path: string, flags: number, mode: number): Promise<{
|
|
20
|
+
write(content: string | Uint8Array): Promise<void>;
|
|
21
|
+
identity(): Promise<Identity>;
|
|
22
|
+
close(): Promise<void>;
|
|
23
|
+
} | null>;
|
|
24
|
+
/** One entry of the pinned directory, open for reading; `null` outside a pinned session. */
|
|
25
|
+
export interface PinnedRead {
|
|
26
|
+
readText(): Promise<string>;
|
|
27
|
+
close(): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Open one entry of the pinned directory and read it through the descriptor.
|
|
31
|
+
*
|
|
32
|
+
* `path` names an entry *of the pinned directory itself*: the worker resolves it against the
|
|
33
|
+
* cwd it pinned, so no later rename of the directory on disk can redirect the read. The flags
|
|
34
|
+
* the caller passes are the ones the worker opens with — `O_NOFOLLOW` included, so a link at
|
|
35
|
+
* the name is refused rather than followed.
|
|
36
|
+
*/
|
|
37
|
+
export declare function openPinnedRead(path: string, flags: number): Promise<PinnedRead | null>;
|
|
38
|
+
/**
|
|
39
|
+
* The real path of one entry of the pinned directory, resolved from the pinned inode.
|
|
40
|
+
*
|
|
41
|
+
* The answer is what the entry is *inside the pinned directory*: the caller learns whether the
|
|
42
|
+
* name is a plain file (the answer names the pinned path itself) or a link, and where that link
|
|
43
|
+
* goes, without resolving the mutable parent a second time in this process. The worker spells
|
|
44
|
+
* the resolution through `CWD_ANCHOR`, so the directory being renamed on disk changes the name
|
|
45
|
+
* in the answer without changing what it describes.
|
|
46
|
+
*/
|
|
47
|
+
export declare function resolvePinned(path: string): Promise<string | null>;
|
|
48
|
+
export {};
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/** Filesystem mutations relative to a process-pinned parent, not a re-resolved path. */
|
|
2
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
|
+
import { basename, dirname } from "node:path";
|
|
4
|
+
import { NaxError } from "#src/infra/nax-error";
|
|
5
|
+
import { runtimeSpawn } from "#src/runtime/index";
|
|
6
|
+
import { pinnedDirectoryWorker } from "./pinned-directory-worker.js";
|
|
7
|
+
const activeDirectory = new AsyncLocalStorage();
|
|
8
|
+
function protocolError(message) {
|
|
9
|
+
return new NaxError(message, "MEMORY_IO_FAILED", { stage: "memory" });
|
|
10
|
+
}
|
|
11
|
+
/** The suffix of a staging directory: the one two-level operand a request may name. */
|
|
12
|
+
const STAGE_SUFFIX = ".tmp";
|
|
13
|
+
/**
|
|
14
|
+
* The path a pinned worker resolves entries through, when the platform has one.
|
|
15
|
+
*
|
|
16
|
+
* The worker's operations are relative, so the kernel resolves them against the cwd *inode* —
|
|
17
|
+
* a rename of the directory cannot redirect them. Resolution to a real path needs an absolute
|
|
18
|
+
* spelling, and a runtime may build one from a cached cwd string (Bun on Linux keeps the
|
|
19
|
+
* spelling after the directory is renamed), which would resolve the name the directory left
|
|
20
|
+
* behind. `/proc/self/cwd` is the magic link to the same inode and is Linux-only; macOS has no
|
|
21
|
+
* traversable equivalent and needs none, because its `realpath` already answers from the inode.
|
|
22
|
+
*/
|
|
23
|
+
const CWD_ANCHOR = process.platform === "linux" ? "/proc/self/cwd" : null;
|
|
24
|
+
/**
|
|
25
|
+
* The operand a request carries, relative to the pinned directory.
|
|
26
|
+
*
|
|
27
|
+
* A name in the pinned directory is one component; the only deeper operand is a name inside
|
|
28
|
+
* one of its staging directories (`<name>.tmp/<name>`), which is how a write reaches the
|
|
29
|
+
* namespace it created for itself. Nothing else is accepted: no `..`, no absolute path and
|
|
30
|
+
* no further nesting.
|
|
31
|
+
*/
|
|
32
|
+
function operandIn(pinnedPath, path) {
|
|
33
|
+
if (dirname(path) === pinnedPath)
|
|
34
|
+
return basename(path);
|
|
35
|
+
const stage = dirname(path);
|
|
36
|
+
if (dirname(stage) === pinnedPath && basename(stage).endsWith(STAGE_SUFFIX)) {
|
|
37
|
+
return `${basename(stage)}/${basename(path)}`;
|
|
38
|
+
}
|
|
39
|
+
throw protocolError(`Path is not in the pinned directory: ${path}`);
|
|
40
|
+
}
|
|
41
|
+
/** Resolve one operand to a request: the same check for a single path and for a rename pair. */
|
|
42
|
+
export function pinnedOperation(path, op, args = []) {
|
|
43
|
+
const directory = activeDirectory.getStore();
|
|
44
|
+
if (!directory)
|
|
45
|
+
return null;
|
|
46
|
+
if (op === "rename") {
|
|
47
|
+
return directory.request(op, [operandIn(directory.path, path), operandIn(directory.path, String(args[0]))]);
|
|
48
|
+
}
|
|
49
|
+
return directory.request(op, [operandIn(directory.path, path), ...args]);
|
|
50
|
+
}
|
|
51
|
+
async function* responses(stream) {
|
|
52
|
+
const reader = stream.getReader();
|
|
53
|
+
const decoder = new TextDecoder();
|
|
54
|
+
let pending = "";
|
|
55
|
+
try {
|
|
56
|
+
while (true) {
|
|
57
|
+
const chunk = await reader.read();
|
|
58
|
+
if (chunk.done)
|
|
59
|
+
return;
|
|
60
|
+
pending += decoder.decode(chunk.value, { stream: true });
|
|
61
|
+
let end = pending.indexOf("\n");
|
|
62
|
+
while (end !== -1) {
|
|
63
|
+
yield pending.slice(0, end);
|
|
64
|
+
pending = pending.slice(end + 1);
|
|
65
|
+
end = pending.indexOf("\n");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
reader.releaseLock();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Spawn first, then verify the child's cwd identity before sending any mutation.
|
|
75
|
+
* A swap during spawn therefore fails closed. Later swaps cannot redirect cwd-relative
|
|
76
|
+
* operations. AsyncLocalStorage keeps concurrent stores/seam wrappers isolated without
|
|
77
|
+
* changing the existing `_atomicWriteDeps` call signatures.
|
|
78
|
+
*/
|
|
79
|
+
export async function withPinnedDirectory(input, action) {
|
|
80
|
+
// Builtins are supplied in the script, not imported inside the serialized function:
|
|
81
|
+
// bundlers/test transforms may otherwise rewrite import() into closure-bound helpers.
|
|
82
|
+
const script = `(${pinnedDirectoryWorker.toString()})(require("node:fs/promises"), {
|
|
83
|
+
createInterface: require("node:readline").createInterface,
|
|
84
|
+
input: process.stdin, write: (text) => process.stdout.write(text),
|
|
85
|
+
anchor: ${JSON.stringify(CWD_ANCHOR)}
|
|
86
|
+
})`;
|
|
87
|
+
const child = runtimeSpawn([process.execPath, "-e", script], {
|
|
88
|
+
cwd: input.path,
|
|
89
|
+
stdin: "pipe",
|
|
90
|
+
stdout: "pipe",
|
|
91
|
+
stderr: "pipe",
|
|
92
|
+
});
|
|
93
|
+
const stderr = new Response(child.stderr).text();
|
|
94
|
+
const lines = responses(child.stdout);
|
|
95
|
+
let queue = Promise.resolve();
|
|
96
|
+
const directory = {
|
|
97
|
+
path: input.path,
|
|
98
|
+
request(op, args) {
|
|
99
|
+
const result = queue.then(async () => {
|
|
100
|
+
child.stdin?.write(`${JSON.stringify({ op, args })}\n`);
|
|
101
|
+
child.stdin?.flush();
|
|
102
|
+
const reply = await lines.next();
|
|
103
|
+
if (reply.done)
|
|
104
|
+
throw protocolError(`Pinned directory worker exited: ${await stderr}`);
|
|
105
|
+
const response = JSON.parse(reply.value);
|
|
106
|
+
if (response.error)
|
|
107
|
+
throw new NaxError(response.error, response.code ?? "MEMORY_IO_FAILED", { stage: "memory", op });
|
|
108
|
+
return response.value;
|
|
109
|
+
});
|
|
110
|
+
queue = result.then(() => undefined, () => undefined);
|
|
111
|
+
return result;
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
try {
|
|
115
|
+
const actual = await directory.request("directory", []);
|
|
116
|
+
if (actual.dev !== input.identity.dev || actual.ino !== input.identity.ino)
|
|
117
|
+
throw input.changed();
|
|
118
|
+
return await activeDirectory.run(directory, action);
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
child.stdin?.end();
|
|
122
|
+
await Promise.all([child.exited, stderr]);
|
|
123
|
+
await lines.return(undefined);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/** Opened stage RPCs use the worker's descriptor table, never a staging pathname. */
|
|
127
|
+
export async function openPinnedStage(path, flags, mode) {
|
|
128
|
+
const directory = activeDirectory.getStore();
|
|
129
|
+
if (!directory)
|
|
130
|
+
return null;
|
|
131
|
+
const descriptor = await pinnedOperation(path, "open", [flags, mode]);
|
|
132
|
+
return {
|
|
133
|
+
write: (content) => directory.request("write", [descriptor, Buffer.from(content).toString("base64")]),
|
|
134
|
+
identity: () => directory.request("identity", [descriptor]),
|
|
135
|
+
close: () => directory.request("close", [descriptor]),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Open one entry of the pinned directory and read it through the descriptor.
|
|
140
|
+
*
|
|
141
|
+
* `path` names an entry *of the pinned directory itself*: the worker resolves it against the
|
|
142
|
+
* cwd it pinned, so no later rename of the directory on disk can redirect the read. The flags
|
|
143
|
+
* the caller passes are the ones the worker opens with — `O_NOFOLLOW` included, so a link at
|
|
144
|
+
* the name is refused rather than followed.
|
|
145
|
+
*/
|
|
146
|
+
export async function openPinnedRead(path, flags) {
|
|
147
|
+
const directory = activeDirectory.getStore();
|
|
148
|
+
if (!directory)
|
|
149
|
+
return null;
|
|
150
|
+
const descriptor = await pinnedOperation(path, "open", [flags, 0o666]);
|
|
151
|
+
return {
|
|
152
|
+
readText: () => directory.request("read", [descriptor]),
|
|
153
|
+
close: () => directory.request("close", [descriptor]),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* The real path of one entry of the pinned directory, resolved from the pinned inode.
|
|
158
|
+
*
|
|
159
|
+
* The answer is what the entry is *inside the pinned directory*: the caller learns whether the
|
|
160
|
+
* name is a plain file (the answer names the pinned path itself) or a link, and where that link
|
|
161
|
+
* goes, without resolving the mutable parent a second time in this process. The worker spells
|
|
162
|
+
* the resolution through `CWD_ANCHOR`, so the directory being renamed on disk changes the name
|
|
163
|
+
* in the answer without changing what it describes.
|
|
164
|
+
*/
|
|
165
|
+
export async function resolvePinned(path) {
|
|
166
|
+
const resolved = await pinnedOperation(path, "resolve");
|
|
167
|
+
return resolved === null ? null : await resolved;
|
|
168
|
+
}
|
package/dist/internal.d.ts
CHANGED
|
@@ -46,6 +46,8 @@ export * from "#src/internal/shell-quote";
|
|
|
46
46
|
export * from "#src/internal/sort";
|
|
47
47
|
export * from "#src/internal/strip-control-chars";
|
|
48
48
|
export * from "#src/internal/thenable";
|
|
49
|
+
export { parseMemoryNoteRecord } from "#src/memory/note";
|
|
50
|
+
export { _memoryStoreDeps } from "#src/memory/store";
|
|
49
51
|
export * from "#src/native/adapter-deps";
|
|
50
52
|
export * from "#src/native/auth";
|
|
51
53
|
export * from "#src/native/client";
|
package/dist/internal.js
CHANGED
|
@@ -46,6 +46,8 @@ export * from "#src/internal/shell-quote";
|
|
|
46
46
|
export * from "#src/internal/sort";
|
|
47
47
|
export * from "#src/internal/strip-control-chars";
|
|
48
48
|
export * from "#src/internal/thenable";
|
|
49
|
+
export { parseMemoryNoteRecord } from "#src/memory/note";
|
|
50
|
+
export { _memoryStoreDeps } from "#src/memory/store";
|
|
49
51
|
export * from "#src/native/adapter-deps";
|
|
50
52
|
export * from "#src/native/auth";
|
|
51
53
|
export * from "#src/native/client";
|