@namzu/sdk 60.0.0 → 60.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -0
- package/dist/agents/QueryAgent.d.ts.map +1 -1
- package/dist/agents/QueryAgent.js +1 -0
- package/dist/agents/QueryAgent.js.map +1 -1
- package/dist/bridge/a2a/mapper.d.ts.map +1 -1
- package/dist/bridge/a2a/mapper.js +19 -0
- package/dist/bridge/a2a/mapper.js.map +1 -1
- package/dist/bridge/acp/update.d.ts.map +1 -1
- package/dist/bridge/acp/update.js +1 -0
- package/dist/bridge/acp/update.js.map +1 -1
- package/dist/bridge/sse/mapper.d.ts.map +1 -1
- package/dist/bridge/sse/mapper.js +28 -0
- package/dist/bridge/sse/mapper.js.map +1 -1
- package/dist/constants/continuation.d.ts +9 -0
- package/dist/constants/continuation.d.ts.map +1 -1
- package/dist/constants/continuation.js +9 -0
- package/dist/constants/continuation.js.map +1 -1
- package/dist/contracts/session/api.d.ts +8 -2
- package/dist/contracts/session/api.d.ts.map +1 -1
- package/dist/manager/session/turn-recorder.d.ts +7 -1
- package/dist/manager/session/turn-recorder.d.ts.map +1 -1
- package/dist/manager/session/turn-recorder.js +20 -0
- package/dist/manager/session/turn-recorder.js.map +1 -1
- package/dist/peers/dir.d.ts.map +1 -1
- package/dist/peers/dir.js +3 -0
- package/dist/peers/dir.js.map +1 -1
- package/dist/provider/collect-chat-completion.d.ts.map +1 -1
- package/dist/provider/collect-chat-completion.js +4 -0
- package/dist/provider/collect-chat-completion.js.map +1 -1
- package/dist/provider/fallback.d.ts.map +1 -1
- package/dist/provider/fallback.js +11 -0
- package/dist/provider/fallback.js.map +1 -1
- package/dist/provider/idle-timeout.d.ts.map +1 -1
- package/dist/provider/idle-timeout.js +5 -0
- package/dist/provider/idle-timeout.js.map +1 -1
- package/dist/provider/mock.d.ts +2 -0
- package/dist/provider/mock.d.ts.map +1 -1
- package/dist/provider/mock.js +16 -8
- package/dist/provider/mock.js.map +1 -1
- package/dist/provider/model-window-suffix.d.ts.map +1 -1
- package/dist/provider/model-window-suffix.js +1 -0
- package/dist/provider/model-window-suffix.js.map +1 -1
- package/dist/provider/retry.d.ts.map +1 -1
- package/dist/provider/retry.js +5 -0
- package/dist/provider/retry.js.map +1 -1
- package/dist/public-runtime.d.ts +1 -0
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +1 -0
- package/dist/public-runtime.js.map +1 -1
- package/dist/runtime/query/events.d.ts.map +1 -1
- package/dist/runtime/query/events.js +17 -0
- package/dist/runtime/query/events.js.map +1 -1
- package/dist/runtime/query/executor.d.ts +23 -0
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +88 -9
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/index.d.ts +14 -0
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +14 -0
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/effort-clamp.d.ts +13 -0
- package/dist/runtime/query/iteration/effort-clamp.d.ts.map +1 -0
- package/dist/runtime/query/iteration/effort-clamp.js +42 -0
- package/dist/runtime/query/iteration/effort-clamp.js.map +1 -0
- package/dist/runtime/query/iteration/index.d.ts +34 -0
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +199 -6
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context.d.ts +2 -0
- package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/context.js.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.js +6 -0
- package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
- package/dist/runtime/query/iteration/stream-turn.d.ts.map +1 -1
- package/dist/runtime/query/iteration/stream-turn.js +6 -0
- package/dist/runtime/query/iteration/stream-turn.js.map +1 -1
- package/dist/runtime/query/review-policy.d.ts.map +1 -1
- package/dist/runtime/query/review-policy.js +12 -2
- package/dist/runtime/query/review-policy.js.map +1 -1
- package/dist/runtime/query/tooling.d.ts +2 -0
- package/dist/runtime/query/tooling.d.ts.map +1 -1
- package/dist/runtime/query/tooling.js +1 -0
- package/dist/runtime/query/tooling.js.map +1 -1
- package/dist/sandbox/provider/local.d.ts.map +1 -1
- package/dist/sandbox/provider/local.js +6 -5
- package/dist/sandbox/provider/local.js.map +1 -1
- package/dist/session/paths.d.ts +15 -22
- package/dist/session/paths.d.ts.map +1 -1
- package/dist/session/paths.js +19 -46
- package/dist/session/paths.js.map +1 -1
- package/dist/store/memory/disk.d.ts.map +1 -1
- package/dist/store/memory/disk.js +1 -1
- package/dist/store/memory/disk.js.map +1 -1
- package/dist/store/memory/markdown.d.ts.map +1 -1
- package/dist/store/memory/markdown.js +1 -1
- package/dist/store/memory/markdown.js.map +1 -1
- package/dist/store/memory/operation-lock.d.ts +20 -4
- package/dist/store/memory/operation-lock.d.ts.map +1 -1
- package/dist/store/memory/operation-lock.js +235 -58
- package/dist/store/memory/operation-lock.js.map +1 -1
- package/dist/store/session-log/core.d.ts +43 -0
- package/dist/store/session-log/core.d.ts.map +1 -1
- package/dist/store/session-log/core.js +190 -3
- package/dist/store/session-log/core.js.map +1 -1
- package/dist/store/session-log/disk.d.ts +1 -1
- package/dist/store/session-log/disk.d.ts.map +1 -1
- package/dist/store/session-log/disk.js +1 -0
- package/dist/store/session-log/disk.js.map +1 -1
- package/dist/store/session-log/index.d.ts +1 -1
- package/dist/store/session-log/index.d.ts.map +1 -1
- package/dist/store/session-log/index.js +1 -1
- package/dist/store/session-log/index.js.map +1 -1
- package/dist/tools/builtins/bash-cwd.d.ts.map +1 -1
- package/dist/tools/builtins/bash-cwd.js +2 -3
- package/dist/tools/builtins/bash-cwd.js.map +1 -1
- package/dist/tools/builtins/read-media.js +3 -3
- package/dist/tools/builtins/read-media.js.map +1 -1
- package/dist/turn/reporter.d.ts.map +1 -1
- package/dist/turn/reporter.js +13 -0
- package/dist/turn/reporter.js.map +1 -1
- package/dist/types/agent/query.d.ts +2 -0
- package/dist/types/agent/query.d.ts.map +1 -1
- package/dist/types/hitl/index.d.ts +8 -0
- package/dist/types/hitl/index.d.ts.map +1 -1
- package/dist/types/hitl/index.js.map +1 -1
- package/dist/types/message/index.d.ts +1 -1
- package/dist/types/message/index.d.ts.map +1 -1
- package/dist/types/message/index.js +1 -0
- package/dist/types/message/index.js.map +1 -1
- package/dist/types/provider/chat.d.ts +19 -0
- package/dist/types/provider/chat.d.ts.map +1 -1
- package/dist/types/provider/config.d.ts +7 -0
- package/dist/types/provider/config.d.ts.map +1 -1
- package/dist/types/provider/index.d.ts +1 -1
- package/dist/types/provider/index.d.ts.map +1 -1
- package/dist/types/provider/interface.d.ts +15 -0
- package/dist/types/provider/interface.d.ts.map +1 -1
- package/dist/types/provider/stream.d.ts +6 -0
- package/dist/types/provider/stream.d.ts.map +1 -1
- package/dist/types/session/events.d.ts +72 -2
- package/dist/types/session/events.d.ts.map +1 -1
- package/dist/types/session/events.js +4 -0
- package/dist/types/session/events.js.map +1 -1
- package/dist/types/session/records.d.ts +34 -34
- package/dist/types/session/records.d.ts.map +1 -1
- package/dist/types/session/records.js +5 -0
- package/dist/types/session/records.js.map +1 -1
- package/dist/types/session/stop-reason.d.ts +11 -0
- package/dist/types/session/stop-reason.d.ts.map +1 -1
- package/dist/utils/temp.d.ts +82 -0
- package/dist/utils/temp.d.ts.map +1 -0
- package/dist/utils/temp.js +196 -0
- package/dist/utils/temp.js.map +1 -0
- package/package.json +1 -1
- package/src/agents/QueryAgent.ts +1 -0
- package/src/bridge/a2a/mapper.ts +22 -0
- package/src/bridge/acp/update.ts +6 -1
- package/src/bridge/sse/mapper.ts +29 -0
- package/src/constants/continuation.ts +13 -0
- package/src/contracts/session/api.ts +8 -0
- package/src/manager/session/turn-recorder.ts +22 -0
- package/src/peers/dir.ts +3 -0
- package/src/provider/collect-chat-completion.ts +3 -0
- package/src/provider/fallback.ts +17 -0
- package/src/provider/idle-timeout.ts +9 -0
- package/src/provider/mock.ts +19 -9
- package/src/provider/model-window-suffix.ts +1 -0
- package/src/provider/retry.ts +9 -0
- package/src/public-runtime.ts +13 -0
- package/src/runtime/query/events.ts +19 -0
- package/src/runtime/query/executor.ts +92 -8
- package/src/runtime/query/index.ts +27 -0
- package/src/runtime/query/iteration/effort-clamp.ts +46 -0
- package/src/runtime/query/iteration/index.ts +260 -6
- package/src/runtime/query/iteration/phases/context.ts +2 -0
- package/src/runtime/query/iteration/phases/tool-review.ts +6 -0
- package/src/runtime/query/iteration/stream-turn.ts +5 -0
- package/src/runtime/query/review-policy.ts +13 -2
- package/src/runtime/query/tooling.ts +3 -0
- package/src/sandbox/provider/local.ts +8 -5
- package/src/session/paths.ts +32 -57
- package/src/store/memory/disk.ts +1 -0
- package/src/store/memory/markdown.ts +1 -0
- package/src/store/memory/operation-lock.ts +288 -57
- package/src/store/session-log/core.ts +235 -3
- package/src/store/session-log/disk.ts +7 -1
- package/src/store/session-log/index.ts +4 -0
- package/src/tools/builtins/bash-cwd.ts +2 -3
- package/src/tools/builtins/read-media.ts +3 -3
- package/src/turn/reporter.ts +14 -0
- package/src/types/agent/query.ts +2 -0
- package/src/types/hitl/index.ts +8 -0
- package/src/types/message/index.ts +5 -3
- package/src/types/provider/chat.ts +20 -0
- package/src/types/provider/config.ts +7 -0
- package/src/types/provider/index.ts +1 -0
- package/src/types/provider/interface.ts +20 -0
- package/src/types/provider/stream.ts +6 -0
- package/src/types/session/events.ts +106 -6
- package/src/types/session/records.ts +5 -0
- package/src/types/session/stop-reason.ts +11 -0
- package/src/utils/temp.ts +250 -0
|
@@ -7,6 +7,8 @@ import { estimateMessageTokens } from '../../../compaction/token-estimate.js'
|
|
|
7
7
|
import {
|
|
8
8
|
AUTO_CONTINUATION_USER_MESSAGE,
|
|
9
9
|
REASONING_CUT_USER_MESSAGE,
|
|
10
|
+
REFUSED_REPLY_CALL_RESULT,
|
|
11
|
+
REFUSED_REPLY_RETRY_MESSAGE,
|
|
10
12
|
} from '../../../constants/continuation.js'
|
|
11
13
|
import { CHARS_PER_TOKEN } from '../../../constants/limits.js'
|
|
12
14
|
import {
|
|
@@ -37,6 +39,7 @@ import {
|
|
|
37
39
|
createAssistantMessage,
|
|
38
40
|
createRuntimeContextMessage,
|
|
39
41
|
createSystemMessage,
|
|
42
|
+
createToolMessage,
|
|
40
43
|
} from '../../../types/message/index.js'
|
|
41
44
|
import { classifyProviderError, readOutputOverflow } from '../../../types/provider/errors.js'
|
|
42
45
|
import type { ChatCompletionResponse } from '../../../types/provider/index.js'
|
|
@@ -71,6 +74,7 @@ import {
|
|
|
71
74
|
projectRequestRichContent,
|
|
72
75
|
} from '../request-rich-content.js'
|
|
73
76
|
import { formatSteeringNote, isOperatorUserMessage } from '../steering.js'
|
|
77
|
+
import { effortForModel } from './effort-clamp.js'
|
|
74
78
|
import { parseNativeCandidate } from './native-output.js'
|
|
75
79
|
import {
|
|
76
80
|
drainArrivedTaskCompletions,
|
|
@@ -225,7 +229,10 @@ export class IterationOrchestrator {
|
|
|
225
229
|
* serving from the member it last moved to, so that member, not the head,
|
|
226
230
|
* is the one whose reading of images counts.
|
|
227
231
|
*/
|
|
228
|
-
private servingModel(model: string): {
|
|
232
|
+
private servingModel(model: string): {
|
|
233
|
+
readonly providerId: string
|
|
234
|
+
readonly model: string
|
|
235
|
+
} {
|
|
229
236
|
const serving = this.ctx.servingMember?.()
|
|
230
237
|
return {
|
|
231
238
|
providerId: serving?.providerId ?? this.ctx.provider.id,
|
|
@@ -291,6 +298,16 @@ export class IterationOrchestrator {
|
|
|
291
298
|
private stopDeferredForOutstandingWork = false
|
|
292
299
|
/** Nudges sent since the model last finished a call without hitting its output cap. */
|
|
293
300
|
private outputContinuations = 0
|
|
301
|
+
/**
|
|
302
|
+
* The model a refused request moved to. Once set it replaces the model the
|
|
303
|
+
* step asked for, for the rest of the turn: the request that was refused
|
|
304
|
+
* would only be refused again.
|
|
305
|
+
*/
|
|
306
|
+
private refusalPinnedModel: string | undefined
|
|
307
|
+
/** The request has already been re-run on another model once this turn. */
|
|
308
|
+
private refusalFallbackUsed = false
|
|
309
|
+
/** The request has already been re-run on the same model once this turn. */
|
|
310
|
+
private refusalRetryUsed = false
|
|
294
311
|
/**
|
|
295
312
|
* `turn_end` (and, for a child, `subagent_stop`) fired when the model
|
|
296
313
|
* finished and no hook asked for more. `finalizeTurn` then does not fire
|
|
@@ -319,7 +336,11 @@ export class IterationOrchestrator {
|
|
|
319
336
|
const feedback: string[] = []
|
|
320
337
|
const ask = async (
|
|
321
338
|
event: 'turn_end' | 'subagent_stop',
|
|
322
|
-
extra: {
|
|
339
|
+
extra: {
|
|
340
|
+
parentSessionId?: SessionId
|
|
341
|
+
parentTurnId?: TurnId
|
|
342
|
+
agentType?: string
|
|
343
|
+
},
|
|
323
344
|
): Promise<void> => {
|
|
324
345
|
const results = await manager.executeHooks(
|
|
325
346
|
event,
|
|
@@ -795,7 +816,7 @@ export class IterationOrchestrator {
|
|
|
795
816
|
// Preparation inference belongs to the turn, not the main-model step.
|
|
796
817
|
usageBefore = { ...recorder.tokenUsage }
|
|
797
818
|
costBefore = { ...recorder.costInfo }
|
|
798
|
-
stepModel = step.model ?? model
|
|
819
|
+
stepModel = this.refusalPinnedModel ?? step.model ?? model
|
|
799
820
|
await selectContextModel(shaping, stepModel)
|
|
800
821
|
// Preserve post-compaction preparation/recall semantics. A changed
|
|
801
822
|
// model needs a second check against its own window; never replay
|
|
@@ -824,7 +845,7 @@ export class IterationOrchestrator {
|
|
|
824
845
|
// still call any of them by name.
|
|
825
846
|
this.ctx.toolExecutor.setStepAllowedTools(stepAllowedTools)
|
|
826
847
|
const enforceToolInputSchema = enforcedModelInputToolNames(this.ctx.tools, llmTools)
|
|
827
|
-
stepModel = step.model ?? model
|
|
848
|
+
stepModel = this.refusalPinnedModel ?? step.model ?? model
|
|
828
849
|
|
|
829
850
|
// The closing directive is appended LAST, after every piece of
|
|
830
851
|
// request-only context below, so it is the final thing the model
|
|
@@ -1051,6 +1072,25 @@ export class IterationOrchestrator {
|
|
|
1051
1072
|
model: requestedMember.model ?? stepModel,
|
|
1052
1073
|
chainIndex: requestedMember.index,
|
|
1053
1074
|
}
|
|
1075
|
+
// The turn's effort was chosen for the turn's own model. A request
|
|
1076
|
+
// moved to another model (a refusal fallback, a live pick) carries
|
|
1077
|
+
// the nearest level that model offers, for this request only.
|
|
1078
|
+
let requestEffort = turnConfig.effort
|
|
1079
|
+
if (requestEffort && stepModel !== model) {
|
|
1080
|
+
requestEffort = effortForModel(
|
|
1081
|
+
this.ctx.provider,
|
|
1082
|
+
stepModel,
|
|
1083
|
+
requestEffort,
|
|
1084
|
+
turnConfig.thinking,
|
|
1085
|
+
)
|
|
1086
|
+
if (requestEffort !== turnConfig.effort) {
|
|
1087
|
+
this.ctx.log.debug('Effort lowered to a level this model offers', {
|
|
1088
|
+
'namzu.runtime.model': stepModel,
|
|
1089
|
+
'namzu.runtime.effort_requested': turnConfig.effort,
|
|
1090
|
+
'namzu.runtime.effort_used': requestEffort,
|
|
1091
|
+
})
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1054
1094
|
const operatorInputAtDispatch = this.latestUserMessage
|
|
1055
1095
|
const latestReviewUserMessage =
|
|
1056
1096
|
(this.ctx.reviewAnswer || this.ctx.structuredOutput?.review) && operatorInputAtDispatch
|
|
@@ -1092,7 +1132,7 @@ export class IterationOrchestrator {
|
|
|
1092
1132
|
outputCapOverride ?? step.maxResponseTokens ?? turnConfig.maxResponseTokens,
|
|
1093
1133
|
cacheControl: { type: 'auto' },
|
|
1094
1134
|
...(turnConfig.thinking ? { thinking: turnConfig.thinking } : {}),
|
|
1095
|
-
...(
|
|
1135
|
+
...(requestEffort ? { effort: requestEffort } : {}),
|
|
1096
1136
|
...(!forceFinalize && turnConfig.webSearch
|
|
1097
1137
|
? { webSearch: turnConfig.webSearch }
|
|
1098
1138
|
: {}),
|
|
@@ -1247,6 +1287,34 @@ export class IterationOrchestrator {
|
|
|
1247
1287
|
...contextFigures,
|
|
1248
1288
|
})
|
|
1249
1289
|
|
|
1290
|
+
// A reply the provider declined to give is its own case. It is not
|
|
1291
|
+
// an empty completion: asking again with the same history on the
|
|
1292
|
+
// same model is refused again, at the full price of the prompt.
|
|
1293
|
+
// Recovered here, before the reply can enter the history.
|
|
1294
|
+
if (response.finishReason === 'content_filter') {
|
|
1295
|
+
iterSpan.setStatus({ code: SpanStatusCode.OK })
|
|
1296
|
+
if (iterationActivity) {
|
|
1297
|
+
this.ctx.activityStore.complete(iterationActivity.id, {
|
|
1298
|
+
content: response.message.content,
|
|
1299
|
+
hasToolCalls: false,
|
|
1300
|
+
})
|
|
1301
|
+
}
|
|
1302
|
+
const outcome = yield* this.recoverFromRefusal({
|
|
1303
|
+
response,
|
|
1304
|
+
messageId,
|
|
1305
|
+
stepModel,
|
|
1306
|
+
servedBy,
|
|
1307
|
+
forceFinalize,
|
|
1308
|
+
limitStopReason: forceFinalize ? guardResult.stopReason : undefined,
|
|
1309
|
+
iterationNum,
|
|
1310
|
+
stepStartedAt,
|
|
1311
|
+
usageBefore,
|
|
1312
|
+
costBefore,
|
|
1313
|
+
})
|
|
1314
|
+
if (outcome === 'retry') continue
|
|
1315
|
+
break
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1250
1318
|
// Durable reasoning and its adapter-private replay envelope ride
|
|
1251
1319
|
// with the turn they belong to. Trimming therefore removes both;
|
|
1252
1320
|
// retaining them gives the target adapter enough evidence to
|
|
@@ -1681,7 +1749,10 @@ export class IterationOrchestrator {
|
|
|
1681
1749
|
[NAMZU.ITERATION]: iterationNum,
|
|
1682
1750
|
'namzu.runtime.finish_reason': response.finishReason,
|
|
1683
1751
|
})
|
|
1684
|
-
closingStopReason = await this.requestFinalResponse(
|
|
1752
|
+
closingStopReason = await this.requestFinalResponse(
|
|
1753
|
+
this.refusalPinnedModel ?? model,
|
|
1754
|
+
'end_turn',
|
|
1755
|
+
)
|
|
1685
1756
|
yield* this.ctx.drainPending()
|
|
1686
1757
|
}
|
|
1687
1758
|
|
|
@@ -2873,6 +2944,179 @@ export class IterationOrchestrator {
|
|
|
2873
2944
|
})
|
|
2874
2945
|
}
|
|
2875
2946
|
|
|
2947
|
+
/**
|
|
2948
|
+
* A reply the provider declined to give.
|
|
2949
|
+
*
|
|
2950
|
+
* The refused reply never enters the history sent next, except on the
|
|
2951
|
+
* same-model retry, where the stopped partial reply stays so the model can
|
|
2952
|
+
* see what to avoid. At most one automatic retry of each kind per turn:
|
|
2953
|
+
*
|
|
2954
|
+
* 1. Another model of the same provider, when the provider names one and
|
|
2955
|
+
* the host has not turned `refusalFallback` off. The model is kept for
|
|
2956
|
+
* the rest of the turn and announced in a `model_refusal` event.
|
|
2957
|
+
* 2. The same model once more, only when the refused reply had partial
|
|
2958
|
+
* text or tool calls: unfinished calls get an error result and the
|
|
2959
|
+
* model is told the reply was stopped.
|
|
2960
|
+
* 3. Otherwise the turn settles `refused`.
|
|
2961
|
+
*/
|
|
2962
|
+
private async *recoverFromRefusal(input: {
|
|
2963
|
+
response: ChatCompletionResponse
|
|
2964
|
+
messageId: MessageId
|
|
2965
|
+
stepModel: string
|
|
2966
|
+
servedBy: StepProvenance
|
|
2967
|
+
forceFinalize: boolean
|
|
2968
|
+
/** The limit that asked for this closing step; it stays the reason the turn ended. */
|
|
2969
|
+
limitStopReason?: StopReason | undefined
|
|
2970
|
+
iterationNum: number
|
|
2971
|
+
stepStartedAt: number
|
|
2972
|
+
usageBefore: TokenUsage
|
|
2973
|
+
costBefore: CostInfo
|
|
2974
|
+
}): AsyncGenerator<SessionEvent, 'retry' | 'settled'> {
|
|
2975
|
+
const { recorder } = this.ctx
|
|
2976
|
+
const { response, servedBy, iterationNum } = input
|
|
2977
|
+
const refusal = response.refusal ?? {}
|
|
2978
|
+
const toolCalls = response.message.toolCalls ?? []
|
|
2979
|
+
const hasPartial =
|
|
2980
|
+
(response.message.content !== null && response.message.content.length > 0) ||
|
|
2981
|
+
toolCalls.length > 0
|
|
2982
|
+
|
|
2983
|
+
this.ctx.log.warn('The model declined to answer', {
|
|
2984
|
+
[NAMZU.TURN_ID]: recorder.turnId,
|
|
2985
|
+
[NAMZU.ITERATION]: iterationNum,
|
|
2986
|
+
'namzu.runtime.model': servedBy.model,
|
|
2987
|
+
'namzu.runtime.refusal_category': refusal.category,
|
|
2988
|
+
'namzu.runtime.has_partial_output': hasPartial,
|
|
2989
|
+
})
|
|
2990
|
+
|
|
2991
|
+
const announce = async (outcome: 'fallback' | 'retry' | 'declined', fallbackModel?: string) => {
|
|
2992
|
+
await this.ctx.emitEvent({
|
|
2993
|
+
type: 'model_refusal',
|
|
2994
|
+
turnId: recorder.turnId,
|
|
2995
|
+
iteration: iterationNum,
|
|
2996
|
+
model: servedBy.model,
|
|
2997
|
+
providerId: servedBy.providerId,
|
|
2998
|
+
outcome,
|
|
2999
|
+
...(fallbackModel !== undefined ? { fallbackModel } : {}),
|
|
3000
|
+
...(refusal.category ? { category: refusal.category } : {}),
|
|
3001
|
+
...(refusal.explanation ? { explanation: refusal.explanation } : {}),
|
|
3002
|
+
})
|
|
3003
|
+
}
|
|
3004
|
+
const closeIteration = async () => {
|
|
3005
|
+
this.recordStep({
|
|
3006
|
+
stepNumber: iterationNum,
|
|
3007
|
+
model: input.stepModel,
|
|
3008
|
+
servedBy,
|
|
3009
|
+
messageId: input.messageId,
|
|
3010
|
+
response,
|
|
3011
|
+
toolResults: [],
|
|
3012
|
+
toolExecutionMs: 0,
|
|
3013
|
+
startedAt: input.stepStartedAt,
|
|
3014
|
+
usageBefore: input.usageBefore,
|
|
3015
|
+
costBefore: input.costBefore,
|
|
3016
|
+
})
|
|
3017
|
+
await this.ctx.emitEvent({
|
|
3018
|
+
type: 'iteration_completed',
|
|
3019
|
+
turnId: recorder.turnId,
|
|
3020
|
+
iteration: iterationNum,
|
|
3021
|
+
hasToolCalls: false,
|
|
3022
|
+
})
|
|
3023
|
+
}
|
|
3024
|
+
|
|
3025
|
+
if (this.ctx.abortController.signal.aborted) {
|
|
3026
|
+
recorder.setStopReason('cancelled')
|
|
3027
|
+
recorder.markCancelled()
|
|
3028
|
+
await closeIteration()
|
|
3029
|
+
yield* this.ctx.drainPending()
|
|
3030
|
+
return 'settled'
|
|
3031
|
+
}
|
|
3032
|
+
|
|
3033
|
+
if (!input.forceFinalize && !this.refusalFallbackUsed && this.ctx.refusalFallback !== 'off') {
|
|
3034
|
+
const next = await this.refusalFallbackModel(input.stepModel, refusal)
|
|
3035
|
+
if (next !== undefined) {
|
|
3036
|
+
this.refusalFallbackUsed = true
|
|
3037
|
+
this.refusalPinnedModel = next
|
|
3038
|
+
await announce('fallback', next)
|
|
3039
|
+
await closeIteration()
|
|
3040
|
+
yield* this.ctx.drainPending()
|
|
3041
|
+
return 'retry'
|
|
3042
|
+
}
|
|
3043
|
+
}
|
|
3044
|
+
|
|
3045
|
+
if (!input.forceFinalize && !this.refusalRetryUsed && hasPartial) {
|
|
3046
|
+
this.refusalRetryUsed = true
|
|
3047
|
+
recorder.pushMessage(
|
|
3048
|
+
withStreamedMessageIdentity(
|
|
3049
|
+
createAssistantMessage(
|
|
3050
|
+
response.message.content,
|
|
3051
|
+
toolCalls.length > 0 ? toolCalls : undefined,
|
|
3052
|
+
response.message.reasoning,
|
|
3053
|
+
response.message.citations,
|
|
3054
|
+
{
|
|
3055
|
+
type: 'model',
|
|
3056
|
+
...servedBy,
|
|
3057
|
+
...(response.message.replayState !== undefined
|
|
3058
|
+
? { replayState: response.message.replayState }
|
|
3059
|
+
: {}),
|
|
3060
|
+
},
|
|
3061
|
+
response.message.textParts,
|
|
3062
|
+
),
|
|
3063
|
+
input.messageId,
|
|
3064
|
+
),
|
|
3065
|
+
)
|
|
3066
|
+
for (const call of toolCalls) {
|
|
3067
|
+
recorder.pushMessage(createToolMessage(REFUSED_REPLY_CALL_RESULT, call.id, true))
|
|
3068
|
+
}
|
|
3069
|
+
recorder.pushMessage(
|
|
3070
|
+
createRuntimeContextMessage(REFUSED_REPLY_RETRY_MESSAGE, 'refusal-retry'),
|
|
3071
|
+
)
|
|
3072
|
+
await announce('retry')
|
|
3073
|
+
await closeIteration()
|
|
3074
|
+
yield* this.ctx.drainPending()
|
|
3075
|
+
return 'retry'
|
|
3076
|
+
}
|
|
3077
|
+
|
|
3078
|
+
recorder.setLastError(
|
|
3079
|
+
`${servedBy.model} declined to answer this message${refusal.explanation ? `: ${refusal.explanation}` : '.'}`,
|
|
3080
|
+
)
|
|
3081
|
+
// A closing step that a limit asked for keeps that limit as the reason
|
|
3082
|
+
// the turn ended; the refusal is still announced.
|
|
3083
|
+
recorder.setStopReason(input.limitStopReason ?? 'refused')
|
|
3084
|
+
await announce('declined')
|
|
3085
|
+
await closeIteration()
|
|
3086
|
+
yield* this.ctx.drainPending()
|
|
3087
|
+
return 'settled'
|
|
3088
|
+
}
|
|
3089
|
+
|
|
3090
|
+
/**
|
|
3091
|
+
* Another model to try after `model` declined, or `undefined` when there is
|
|
3092
|
+
* none. Only the provider can say: it names its own recommendation or its
|
|
3093
|
+
* own declared order, checked against what this credential can serve. A
|
|
3094
|
+
* chain member declared with a model of its own cannot be swapped, because
|
|
3095
|
+
* the chain would put its own model back on the request.
|
|
3096
|
+
*/
|
|
3097
|
+
private async refusalFallbackModel(
|
|
3098
|
+
model: string,
|
|
3099
|
+
refusal: NonNullable<ChatCompletionResponse['refusal']>,
|
|
3100
|
+
): Promise<string | undefined> {
|
|
3101
|
+
if (this.ctx.servingMember?.()?.model !== undefined) return undefined
|
|
3102
|
+
const pick = this.ctx.provider.refusalFallbackFor
|
|
3103
|
+
if (!pick) return undefined
|
|
3104
|
+
try {
|
|
3105
|
+
const next = await pick.call(
|
|
3106
|
+
this.ctx.provider,
|
|
3107
|
+
model,
|
|
3108
|
+
refusal,
|
|
3109
|
+
this.ctx.abortController.signal,
|
|
3110
|
+
)
|
|
3111
|
+
return next !== undefined && next.length > 0 && next !== model ? next : undefined
|
|
3112
|
+
} catch (err) {
|
|
3113
|
+
this.ctx.log.warn('The provider could not name a model to retry a refusal on', {
|
|
3114
|
+
'exception.message': toErrorMessage(err),
|
|
3115
|
+
})
|
|
3116
|
+
return undefined
|
|
3117
|
+
}
|
|
3118
|
+
}
|
|
3119
|
+
|
|
2876
3120
|
private async requestFinalResponse(
|
|
2877
3121
|
model: string,
|
|
2878
3122
|
reason: StopReason,
|
|
@@ -2980,6 +3224,16 @@ export class IterationOrchestrator {
|
|
|
2980
3224
|
model: servedBy.model,
|
|
2981
3225
|
})
|
|
2982
3226
|
|
|
3227
|
+
// A closing request the provider declined has nothing to add, and its
|
|
3228
|
+
// empty reply must not be stored or sent again.
|
|
3229
|
+
if (response.finishReason === 'content_filter') {
|
|
3230
|
+
this.ctx.log.warn('The closing response was declined by the model', {
|
|
3231
|
+
[NAMZU.TURN_ID]: this.ctx.recorder.turnId,
|
|
3232
|
+
'namzu.runtime.model': servedBy.model,
|
|
3233
|
+
})
|
|
3234
|
+
return undefined
|
|
3235
|
+
}
|
|
3236
|
+
|
|
2983
3237
|
const finalMessageId = generateMessageId()
|
|
2984
3238
|
const assistantMsg = withStreamedMessageIdentity(
|
|
2985
3239
|
createAssistantMessage(
|
|
@@ -224,6 +224,8 @@ export interface IterationContext {
|
|
|
224
224
|
readonly compactionConfig?: CompactionConfig
|
|
225
225
|
/** See `QueryParams.maxOutputContinuations`. */
|
|
226
226
|
readonly maxOutputContinuations?: number
|
|
227
|
+
/** See `QueryParams.refusalFallback`. */
|
|
228
|
+
readonly refusalFallback?: 'auto' | 'off'
|
|
227
229
|
|
|
228
230
|
/**
|
|
229
231
|
* What the driver said this model's context window is, resolved once.
|
|
@@ -238,6 +238,12 @@ export async function* runToolReview(
|
|
|
238
238
|
...(requiresApproval ? { requiresApproval: true as const } : {}),
|
|
239
239
|
...(escalation ? { escalation } : {}),
|
|
240
240
|
...(hookPermission ? { hookPermission } : {}),
|
|
241
|
+
...('scratchpadWrite' in tc && tc.scratchpadWrite
|
|
242
|
+
? { scratchpadWrite: true as const }
|
|
243
|
+
: {}),
|
|
244
|
+
...('scratchpadWrite' in tc && tc.scratchpadWrite
|
|
245
|
+
? { scratchpadWrite: true as const }
|
|
246
|
+
: {}),
|
|
241
247
|
}
|
|
242
248
|
})
|
|
243
249
|
}
|
|
@@ -284,6 +284,8 @@ export async function* streamProviderTurn(
|
|
|
284
284
|
let reportedFinishReason: ChatCompletionResponse['finishReason'] | undefined
|
|
285
285
|
// Which limit a 'length' finish reached, when it was the context window.
|
|
286
286
|
let finishDetail: ChatCompletionResponse['finishDetail']
|
|
287
|
+
// What the provider said about a refusal, kept with the finish that carried it.
|
|
288
|
+
let refusal: ChatCompletionResponse['refusal']
|
|
287
289
|
let usage: ChatCompletionResponse['usage'] = {
|
|
288
290
|
promptTokens: 0,
|
|
289
291
|
completionTokens: 0,
|
|
@@ -416,6 +418,7 @@ export async function* streamProviderTurn(
|
|
|
416
418
|
finishReason = 'stop'
|
|
417
419
|
reportedFinishReason = undefined
|
|
418
420
|
finishDetail = undefined
|
|
421
|
+
refusal = undefined
|
|
419
422
|
streamedLength = 0
|
|
420
423
|
lastOutputCall = undefined
|
|
421
424
|
usage = {
|
|
@@ -766,6 +769,7 @@ export async function* streamProviderTurn(
|
|
|
766
769
|
reportedFinishReason = chunk.finishReason
|
|
767
770
|
finishDetail = chunk.finishReason === 'length' ? chunk.finishDetail : undefined
|
|
768
771
|
}
|
|
772
|
+
if (chunk.refusal) refusal = chunk.refusal
|
|
769
773
|
// Merge (per-field max), not last-write-wins: a late usage frame that
|
|
770
774
|
// omits input/cache tokens must not zero the counts seen earlier in the
|
|
771
775
|
// stream, which would under-report this turn's accumulated usage.
|
|
@@ -1061,6 +1065,7 @@ export async function* streamProviderTurn(
|
|
|
1061
1065
|
},
|
|
1062
1066
|
finishReason: effectiveFinishReason,
|
|
1063
1067
|
...(effectiveFinishReason === 'length' && finishDetail ? { finishDetail } : {}),
|
|
1068
|
+
...(effectiveFinishReason === 'content_filter' && refusal ? { refusal } : {}),
|
|
1064
1069
|
usage,
|
|
1065
1070
|
}
|
|
1066
1071
|
|
|
@@ -1051,8 +1051,19 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
|
|
|
1051
1051
|
mode === 'plan' &&
|
|
1052
1052
|
options.planModeWrite?.(tc) === true &&
|
|
1053
1053
|
tc.authorization?.decision !== 'deny'
|
|
1054
|
+
// A file written inside the session's own scratch folder, in any mode but
|
|
1055
|
+
// `strict`: the folder is the session's, so it is not a change anyone
|
|
1056
|
+
// reviews, and not "outside the roots". A rule that asked for review keeps
|
|
1057
|
+
// the call in the batch; a deny still refuses it.
|
|
1058
|
+
const isScratchpadWrite = (tc: ToolCallSummary): boolean =>
|
|
1059
|
+
mode !== 'strict' &&
|
|
1060
|
+
tc.scratchpadWrite === true &&
|
|
1061
|
+
tc.escalation === undefined &&
|
|
1062
|
+
tc.requiresApproval !== true &&
|
|
1063
|
+
!tc.authorization?.explicitReview &&
|
|
1064
|
+
tc.authorization?.decision !== 'deny'
|
|
1054
1065
|
const reviewable = request.toolCalls.filter(
|
|
1055
|
-
(tc) => !(isPlanFileWrite(tc) && !tc.authorization?.explicitReview),
|
|
1066
|
+
(tc) => !(isPlanFileWrite(tc) && !tc.authorization?.explicitReview) && !isScratchpadWrite(tc),
|
|
1056
1067
|
)
|
|
1057
1068
|
if (!batchNeedsReview(reviewable, exempt)) {
|
|
1058
1069
|
return { action: 'approve_tools' }
|
|
@@ -1089,7 +1100,7 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
|
|
|
1089
1100
|
if (
|
|
1090
1101
|
mode === 'plan' &&
|
|
1091
1102
|
!onlyReadsOutsideRoots(
|
|
1092
|
-
reviewable.filter((tc) => !isPlanFileWrite(tc)),
|
|
1103
|
+
reviewable.filter((tc) => !isPlanFileWrite(tc) && !isScratchpadWrite(tc)),
|
|
1093
1104
|
exempt,
|
|
1094
1105
|
)
|
|
1095
1106
|
) {
|
|
@@ -28,6 +28,8 @@ export interface ToolingBootstrapConfig {
|
|
|
28
28
|
workingDirectory: string
|
|
29
29
|
/** See `QueryParams.additionalDirectories`. */
|
|
30
30
|
additionalDirectories?: readonly string[]
|
|
31
|
+
/** See `ToolExecutorConfig.scratchpadDirectory`. */
|
|
32
|
+
scratchpadDirectory?: string
|
|
31
33
|
/** See `QueryParams.outsideRootAccess`. */
|
|
32
34
|
outsideRootAccess?: 'refuse' | 'review'
|
|
33
35
|
/** See `QueryParams.sandboxEscape`. */
|
|
@@ -99,6 +101,7 @@ export class ToolingBootstrap {
|
|
|
99
101
|
...(config.additionalDirectories?.length
|
|
100
102
|
? { additionalDirectories: config.additionalDirectories }
|
|
101
103
|
: {}),
|
|
104
|
+
...(config.scratchpadDirectory ? { scratchpadDirectory: config.scratchpadDirectory } : {}),
|
|
102
105
|
...(config.outsideRootAccess ? { outsideRootAccess: config.outsideRootAccess } : {}),
|
|
103
106
|
...(config.sandboxEscape ? { sandboxEscape: config.sandboxEscape } : {}),
|
|
104
107
|
permissionMode: config.permissionMode,
|
|
@@ -53,6 +53,7 @@ import {
|
|
|
53
53
|
applyEnvironmentOverrides,
|
|
54
54
|
pickEnvironmentEntries,
|
|
55
55
|
} from '../../utils/process-environment.js'
|
|
56
|
+
import { createTempDir, resolveTempBase } from '../../utils/temp.js'
|
|
56
57
|
import { walkFilesLocally } from '../file-walk.js'
|
|
57
58
|
import { assertIsolation, describeIsolation } from '../isolation.js'
|
|
58
59
|
import type { PtyLoader } from '../terminal.js'
|
|
@@ -317,7 +318,7 @@ function detectEnvironment(): DetectedEnvironment {
|
|
|
317
318
|
// hosts that mediate namespace helpers and can claim a tier production
|
|
318
319
|
// cannot actually drive.
|
|
319
320
|
const bwrap = resolveTrustedWrapper('bwrap')
|
|
320
|
-
if (bwrap !== undefined && probeBwrapSpawn(bwrap, buildBwrapArgs(
|
|
321
|
+
if (bwrap !== undefined && probeBwrapSpawn(bwrap, buildBwrapArgs(resolveTempBase()))) {
|
|
321
322
|
return { environment: 'linux-bwrap', wrapperCommand: bwrap }
|
|
322
323
|
}
|
|
323
324
|
|
|
@@ -329,7 +330,10 @@ function detectEnvironment(): DetectedEnvironment {
|
|
|
329
330
|
|
|
330
331
|
if (platform === 'darwin') {
|
|
331
332
|
const seatbelt = resolveTrustedWrapper('sandbox-exec')
|
|
332
|
-
if (
|
|
333
|
+
if (
|
|
334
|
+
seatbelt !== undefined &&
|
|
335
|
+
probeSandboxSpawn(seatbelt, buildSeatbeltArgs(resolveTempBase()))
|
|
336
|
+
) {
|
|
333
337
|
return { environment: 'macos-seatbelt', wrapperCommand: seatbelt }
|
|
334
338
|
}
|
|
335
339
|
}
|
|
@@ -1091,7 +1095,7 @@ class LocalSandbox implements Sandbox {
|
|
|
1091
1095
|
exitCode,
|
|
1092
1096
|
stderr: stderr.text,
|
|
1093
1097
|
timedOut,
|
|
1094
|
-
allowedRoots: [...this.roots, tmpdir(), '/tmp', '/var/tmp', '/dev'],
|
|
1098
|
+
allowedRoots: [...this.roots, tmpdir(), resolveTempBase(), '/tmp', '/var/tmp', '/dev'],
|
|
1095
1099
|
})
|
|
1096
1100
|
resolvePromise({
|
|
1097
1101
|
exitCode,
|
|
@@ -1229,8 +1233,7 @@ export class LocalSandboxProvider implements SandboxProvider {
|
|
|
1229
1233
|
}
|
|
1230
1234
|
} else {
|
|
1231
1235
|
// No workspace was named: this allocation owns a fresh temporary root.
|
|
1232
|
-
const
|
|
1233
|
-
const rawDir = await mkdtemp(join(tmpdir(), SANDBOX_TEMP_DIR_PREFIX))
|
|
1236
|
+
const rawDir = await createTempDir(SANDBOX_TEMP_DIR_PREFIX)
|
|
1234
1237
|
rootDir = canonicalizePath(rawDir)
|
|
1235
1238
|
removeRootOnDestroy = true
|
|
1236
1239
|
}
|
package/src/session/paths.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto'
|
|
2
|
-
import { lstatSync, mkdirSync } from 'node:fs'
|
|
3
2
|
import { link, mkdir, open, readFile, realpath, unlink } from 'node:fs/promises'
|
|
4
|
-
import { tmpdir, userInfo } from 'node:os'
|
|
5
3
|
import { basename, dirname, join } from 'node:path'
|
|
6
4
|
import type {
|
|
7
5
|
CheckpointId,
|
|
@@ -14,6 +12,12 @@ import type {
|
|
|
14
12
|
} from '../types/ids/index.js'
|
|
15
13
|
import { type ProjectDocument, ProjectDocumentSchema } from '../types/session/records.js'
|
|
16
14
|
import { type EntityIdKind, asSessionId, generateProjectId, isEntityId } from '../utils/id.js'
|
|
15
|
+
import {
|
|
16
|
+
type PrivateTempRootOptions,
|
|
17
|
+
TempDirectoryError,
|
|
18
|
+
privateTempRoot,
|
|
19
|
+
privateTempRootPath,
|
|
20
|
+
} from '../utils/temp.js'
|
|
17
21
|
import { uuidv7 } from '../utils/uuidv7.js'
|
|
18
22
|
|
|
19
23
|
/**
|
|
@@ -243,68 +247,26 @@ export async function ensureProject(options: EnsureProjectOptions): Promise<Ensu
|
|
|
243
247
|
)
|
|
244
248
|
}
|
|
245
249
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
readonly tmpdir?: string
|
|
249
|
-
/** Default `process.platform`. */
|
|
250
|
-
readonly platform?: NodeJS.Platform
|
|
251
|
-
/**
|
|
252
|
-
* Default `process.getuid`. Pass `undefined` explicitly to model a
|
|
253
|
-
* platform without it (win32).
|
|
254
|
-
*/
|
|
255
|
-
readonly getuid?: (() => number) | undefined
|
|
256
|
-
/** Default `os.userInfo().username`. */
|
|
257
|
-
readonly username?: () => string
|
|
258
|
-
}
|
|
250
|
+
/** See {@link PrivateTempRootOptions}; `tmpdir` is the base. */
|
|
251
|
+
export type TempRootOptions = PrivateTempRootOptions
|
|
259
252
|
|
|
260
253
|
/**
|
|
261
|
-
* The per-user temporary root,
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
* `%TEMP%` is already per user: a symlink or junction is refused and the
|
|
268
|
-
* owner check is skipped.
|
|
254
|
+
* The per-user temporary root, `<base>/namzu-<user>`, created if missing,
|
|
255
|
+
* where the base is `NAMZU_TMPDIR` or `os.tmpdir()`. A root that is a symlink,
|
|
256
|
+
* not a directory, owned by another uid or open to others is refused with a
|
|
257
|
+
* {@link SessionPathError}; one that cannot be created raises the
|
|
258
|
+
* {@link TempDirectoryError} that names the directory and `NAMZU_TMPDIR`.
|
|
259
|
+
* See {@link privateTempRoot}.
|
|
269
260
|
*/
|
|
270
261
|
export function tempRoot(options: TempRootOptions = {}): string {
|
|
271
|
-
const platform = options.platform ?? process.platform
|
|
272
|
-
const getuid = 'getuid' in options ? options.getuid : process.getuid?.bind(process)
|
|
273
|
-
let user = 'user'
|
|
274
|
-
if (typeof getuid === 'function') {
|
|
275
|
-
user = String(getuid())
|
|
276
|
-
} else {
|
|
277
|
-
try {
|
|
278
|
-
const name = (options.username ?? (() => userInfo().username))()
|
|
279
|
-
if (name.length > 0) user = sha256Hex(name).slice(0, 12)
|
|
280
|
-
} catch {
|
|
281
|
-
// No user database entry: fall back to the fixed segment.
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
const root = join(options.tmpdir ?? tmpdir(), `namzu-${user}`)
|
|
285
262
|
try {
|
|
286
|
-
|
|
263
|
+
return privateTempRoot(options)
|
|
287
264
|
} catch (error) {
|
|
288
|
-
if (
|
|
289
|
-
|
|
290
|
-
const entry = lstatSync(root)
|
|
291
|
-
if (entry.isSymbolicLink()) {
|
|
292
|
-
throw new SessionPathError(`Refusing temporary root ${root}: it is a symbolic link.`)
|
|
293
|
-
}
|
|
294
|
-
if (!entry.isDirectory()) {
|
|
295
|
-
throw new SessionPathError(`Refusing temporary root ${root}: it is not a directory.`)
|
|
296
|
-
}
|
|
297
|
-
if (platform !== 'win32' && typeof getuid === 'function') {
|
|
298
|
-
if (entry.uid !== getuid()) {
|
|
299
|
-
throw new SessionPathError(`Refusing temporary root ${root}: it belongs to uid ${entry.uid}.`)
|
|
300
|
-
}
|
|
301
|
-
if ((entry.mode & 0o077) !== 0) {
|
|
302
|
-
throw new SessionPathError(
|
|
303
|
-
`Refusing temporary root ${root}: mode ${(entry.mode & 0o777).toString(8)} lets other users in; it must be 700.`,
|
|
304
|
-
)
|
|
265
|
+
if (error instanceof TempDirectoryError && error.reason === 'refused') {
|
|
266
|
+
throw new SessionPathError(error.message)
|
|
305
267
|
}
|
|
268
|
+
throw error
|
|
306
269
|
}
|
|
307
|
-
return root
|
|
308
270
|
}
|
|
309
271
|
|
|
310
272
|
/** Every path of one project's layout. Pure: nothing is created or read. */
|
|
@@ -451,9 +413,22 @@ export class SessionPaths {
|
|
|
451
413
|
return join(this.sessionDir(locator), 'lease.json')
|
|
452
414
|
}
|
|
453
415
|
|
|
454
|
-
/**
|
|
416
|
+
/** `<temp base>/namzu-<user>/<slug>/<session-id>/scratchpad`. Not created; the per-user root is. */
|
|
455
417
|
tempDir(sessionId: SessionId): string {
|
|
456
418
|
this.#tempRoot ??= tempRoot()
|
|
457
419
|
return join(this.#tempRoot, this.slug, checkId(sessionId, 'session'), 'scratchpad')
|
|
458
420
|
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Where {@link tempDir} is, creating and checking nothing, not even the
|
|
424
|
+
* per-user root: for a caller that makes the folder only when it is first used.
|
|
425
|
+
*/
|
|
426
|
+
scratchpadPath(sessionId: SessionId): string {
|
|
427
|
+
return join(
|
|
428
|
+
this.#tempRoot ?? privateTempRootPath(),
|
|
429
|
+
this.slug,
|
|
430
|
+
checkId(sessionId, 'session'),
|
|
431
|
+
'scratchpad',
|
|
432
|
+
)
|
|
433
|
+
}
|
|
459
434
|
}
|
package/src/store/memory/disk.ts
CHANGED
|
@@ -261,6 +261,7 @@ export class DiskMemoryStore implements ConditionalMemoryStore {
|
|
|
261
261
|
const release = await acquireMemoryOperationLock(
|
|
262
262
|
join(dirname(location.indexPath), 'operation.lock'),
|
|
263
263
|
this.lockTimeoutMs,
|
|
264
|
+
{ logger: this.log },
|
|
264
265
|
)
|
|
265
266
|
try {
|
|
266
267
|
await this.reloadIndex(location)
|
|
@@ -418,6 +418,7 @@ export class MarkdownMemoryStore implements ConditionalMemoryStore {
|
|
|
418
418
|
const release = await acquireMemoryOperationLock(
|
|
419
419
|
join(dir, 'operation.lock'),
|
|
420
420
|
this.lockTimeoutMs,
|
|
421
|
+
{ logger: this.log },
|
|
421
422
|
)
|
|
422
423
|
try {
|
|
423
424
|
const { loaded, superseded } = await this.load(dir, mode.importing ?? false)
|