@code-yeongyu/senpi 2026.11.6-2 → 2026.11.9-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 +64 -0
- package/dist/bundle/chunks/{bun-extension-importer-RCGPQTI2.js → bun-extension-importer-GQ6IKEXD.js} +2 -2
- package/dist/bundle/chunks/{chunk-RQ6CHXQ2.js → chunk-3TZQXZKH.js} +1 -1
- package/dist/bundle/chunks/{chunk-QN2BHP3M.js → chunk-44BJPS5I.js} +1 -1
- package/dist/bundle/chunks/{chunk-WGRKM3N6.js → chunk-45MVD7BU.js} +1 -1
- package/dist/bundle/chunks/chunk-4JIBEZZE.js +106 -0
- package/dist/bundle/chunks/{chunk-CF5OGBC4.js → chunk-4NYNMAYF.js} +1 -1
- package/dist/bundle/chunks/{chunk-X5ZVYNOL.js → chunk-4UR6UFBH.js} +1 -1
- package/dist/bundle/chunks/{chunk-LNPAT22L.js → chunk-527Q2UYM.js} +9 -8
- package/dist/bundle/chunks/{chunk-HLM46XEB.js → chunk-622VYTRI.js} +1 -1
- package/dist/bundle/chunks/{chunk-D5MRJWUK.js → chunk-7CFORZPE.js} +221 -229
- package/dist/bundle/chunks/{chunk-GKAYPJ4E.js → chunk-7PROV6BB.js} +1 -1
- package/dist/bundle/chunks/{chunk-4E7GDG4H.js → chunk-7VMEE62L.js} +1 -1
- package/dist/bundle/chunks/{chunk-C7OKUC7L.js → chunk-K3UHGDUQ.js} +1 -1
- package/dist/bundle/chunks/{chunk-7III44PV.js → chunk-LHSWAJJL.js} +1 -1
- package/dist/bundle/chunks/{chunk-47PPZKPO.js → chunk-LM6DOWQH.js} +4 -4
- package/dist/bundle/chunks/chunk-LPYBKMPT.js +2 -0
- package/dist/bundle/chunks/{chunk-2FDW2LQL.js → chunk-M7GF73FO.js} +2 -2
- package/dist/bundle/chunks/{chunk-ECW7VBMB.js → chunk-NDSWTAYJ.js} +1 -1
- package/dist/bundle/chunks/{chunk-GSKCWXXG.js → chunk-Q6NR6644.js} +1 -1
- package/dist/bundle/chunks/{chunk-TIPKAMSS.js → chunk-SGAFIMAU.js} +1 -1
- package/dist/bundle/chunks/{chunk-7PSBD5KS.js → chunk-STGOYIXC.js} +1 -1
- package/dist/bundle/chunks/{chunk-DSWDJ7HT.js → chunk-WQ4RBPMS.js} +1 -1
- package/dist/bundle/chunks/chunk-XZDCMDUH.js +2 -0
- package/dist/bundle/chunks/{chunk-GNSMNSL7.js → chunk-Y5L4UFMI.js} +2 -2
- package/dist/bundle/chunks/{cli-main-T74YJGIF.js → cli-main-XBEFQW5A.js} +1 -1
- package/dist/bundle/chunks/{dispatch-BAK2YZKD.js → dispatch-VDIYSXQN.js} +1 -1
- package/dist/bundle/chunks/{hand-entry-AYRUGE2A.js → hand-entry-QFEY3JMQ.js} +1 -1
- package/dist/bundle/chunks/{headless-host-ES74ZB6M.js → headless-host-YXUGONR3.js} +1 -1
- package/dist/bundle/chunks/{host-command-YRXZE4TV.js → host-command-VHX2ILLM.js} +1 -1
- package/dist/bundle/chunks/{host-session-command-N7IN7ZDV.js → host-session-command-P3MRW63Z.js} +1 -1
- package/dist/bundle/chunks/interactive-mode-YGL3QHU6.js +2 -0
- package/dist/bundle/chunks/main-5SD4DHMC.js +2 -0
- package/dist/bundle/chunks/{multi-session-host-XSUJDHFZ.js → multi-session-host-PGRHY3JP.js} +20 -17
- package/dist/bundle/chunks/{package-manager-cli-GLUNPQJU.js → package-manager-cli-SADLU7QX.js} +1 -1
- package/dist/bundle/chunks/{role-RRFFHZF5.js → role-4BUU46VW.js} +3 -3
- package/dist/bundle/chunks/{rotation-stream-BN2YK2MC.js → rotation-stream-TODBIHXJ.js} +1 -1
- package/dist/bundle/chunks/rpc-mode-27TC7W6K.js +2 -0
- package/dist/bundle/chunks/{run-attached-tui-W236SEOR.js → run-attached-tui-MXCC5IZR.js} +1 -1
- package/dist/bundle/chunks/{runtime-session-OBXYK4PI.js → runtime-session-VO3X4OE3.js} +1 -1
- package/dist/bundle/chunks/{schedule-command-B2EJTBOE.js → schedule-command-MX56D6UE.js} +1 -1
- package/dist/bundle/chunks/{session-control-endpoint-YIMLHGLG.js → session-control-endpoint-IL6ERD4U.js} +1 -1
- package/dist/bundle/chunks/{session-picker-E6ZONUJB.js → session-picker-6VRC25MJ.js} +1 -1
- package/dist/bundle/chunks/session-worker.js +114 -110
- package/dist/bundle/cli.js +2 -2
- 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-runtime.d.ts +2 -0
- package/dist/core/agent-session-runtime.js +9 -2
- package/dist/core/agent-session-services.d.ts +2 -0
- package/dist/core/agent-session-services.js +1 -0
- package/dist/core/agent-session.d.ts +32 -3
- package/dist/core/agent-session.js +142 -29
- package/dist/core/credential-pool/rotation-slots.d.ts +3 -3
- package/dist/core/credential-pool/state-store.d.ts +1 -1
- package/dist/core/egress/classify.d.ts +8 -3
- package/dist/core/egress/classify.js +177 -158
- package/dist/core/egress/console-redactor.d.ts +7 -0
- package/dist/core/egress/console-redactor.js +74 -0
- package/dist/core/egress/credentials.d.ts +73 -0
- package/dist/core/egress/credentials.js +20 -0
- package/dist/core/egress/device-custody.d.ts +1 -1
- package/dist/core/egress/device-custody.js +18 -15
- package/dist/core/egress/device-proxy-call.d.ts +59 -0
- package/dist/core/egress/device-proxy-call.js +148 -0
- package/dist/core/egress/device-proxy-lost-calls.d.ts +27 -0
- package/dist/core/egress/device-proxy-lost-calls.js +28 -0
- package/dist/core/egress/device-proxy-ports.d.ts +49 -0
- package/dist/core/egress/device-proxy-ports.js +2 -0
- package/dist/core/egress/device-proxy-request.d.ts +34 -0
- package/dist/core/egress/device-proxy-request.js +69 -0
- package/dist/core/egress/device-proxy-waits.d.ts +40 -0
- package/dist/core/egress/device-proxy-waits.js +67 -0
- package/dist/core/egress/device-proxy.d.ts +3 -0
- package/dist/core/egress/device-proxy.js +214 -0
- package/dist/core/egress/endpoint.js +2 -1
- package/dist/core/egress/entry-verify.d.ts +13 -0
- package/dist/core/egress/entry-verify.js +33 -0
- package/dist/core/egress/hpke.d.ts +63 -0
- package/dist/core/egress/hpke.js +192 -0
- package/dist/core/egress/key-policy-template.d.ts +7 -1
- package/dist/core/egress/ledger-schema.d.ts +80 -0
- package/dist/core/egress/ledger-schema.js +117 -0
- package/dist/core/egress/ledger.d.ts +55 -0
- package/dist/core/egress/ledger.js +159 -0
- package/dist/core/egress/pinned-infra.d.ts +23 -2
- package/dist/core/egress/pinned-infra.js +9 -2
- package/dist/core/egress/placement-claim.d.ts +69 -0
- package/dist/core/egress/placement-claim.js +115 -0
- package/dist/core/egress/pool-state.d.ts +18 -0
- package/dist/core/egress/pool-state.js +5 -0
- package/dist/core/egress/redact.d.ts +10 -0
- package/dist/core/egress/redact.js +134 -0
- package/dist/core/egress/sealed-format.d.ts +101 -0
- package/dist/core/egress/sealed-format.js +174 -0
- package/dist/core/egress/sealed-source.d.ts +35 -0
- package/dist/core/egress/sealed-source.js +140 -0
- package/dist/core/egress/verified-keys.js +1 -1
- package/dist/core/egress/wire.d.ts +228 -0
- package/dist/core/egress/wire.js +49 -0
- package/dist/core/extensions/builtin/ask-user/escape-residue.d.ts +50 -0
- package/dist/core/extensions/builtin/ask-user/escape-residue.js +113 -0
- package/dist/core/extensions/builtin/ask-user/extension.js +7 -1
- package/dist/core/extensions/builtin/ask-user/resume.js +2 -1
- package/dist/core/extensions/builtin/ask-user/tool.d.ts +3 -0
- package/dist/core/extensions/builtin/ask-user/tool.js +8 -0
- package/dist/core/extensions/builtin/goal/index.js +46 -0
- package/dist/core/extensions/builtin/goal/stop-pause.d.ts +1 -0
- package/dist/core/extensions/builtin/goal/stop-pause.js +4 -2
- package/dist/core/extensions/builtin/monitor-state-event.d.ts +26 -0
- package/dist/core/extensions/builtin/monitor-state-event.js +2 -0
- package/dist/core/extensions/builtin/permission-system/index.js +10 -4
- package/dist/core/extensions/builtin/permission-system/preset-registry.d.ts +2 -0
- package/dist/core/extensions/builtin/permission-system/preset-registry.js +11 -0
- package/dist/core/extensions/builtin/permission-system/settings.d.ts +2 -0
- package/dist/core/extensions/builtin/permission-system/settings.js +10 -1
- package/dist/core/extensions/builtin/terminal/background-age.d.ts +25 -0
- package/dist/core/extensions/builtin/terminal/background-age.js +91 -0
- package/dist/core/extensions/builtin/terminal/extension-state.d.ts +1 -1
- package/dist/core/extensions/builtin/terminal/extension-state.js +21 -3
- package/dist/core/extensions/builtin/terminal/notify.d.ts +6 -0
- package/dist/core/extensions/builtin/terminal/notify.js +39 -5
- package/dist/core/extensions/builtin/terminal/session-bundle.d.ts +9 -2
- package/dist/core/extensions/builtin/terminal/session-bundle.js +36 -1
- package/dist/core/extensions/builtin/terminal/settings.d.ts +2 -0
- package/dist/core/extensions/builtin/terminal/settings.js +8 -0
- package/dist/core/extensions/builtin/terminal/tools/background-auto-reap.d.ts +15 -0
- package/dist/core/extensions/builtin/terminal/tools/background-auto-reap.js +87 -0
- package/dist/core/extensions/builtin/terminal/tools/bash.d.ts +3 -1
- package/dist/core/extensions/builtin/terminal/tools/bash.js +44 -15
- package/dist/core/extensions/builtin/terminal/tools/child-placement.d.ts +8 -0
- package/dist/core/extensions/builtin/terminal/tools/child-placement.js +43 -6
- package/dist/core/extensions/builtin/terminal/tools/context.d.ts +14 -2
- package/dist/core/extensions/bun-extension-importer.js +4 -3
- package/dist/core/extensions/bun-extension-load-turns.d.ts +4 -0
- package/dist/core/extensions/bun-extension-load-turns.js +45 -0
- package/dist/core/extensions/bun-extension-registry.d.ts +1 -0
- package/dist/core/extensions/bun-extension-registry.js +19 -1
- package/dist/core/extensions/loader.js +5 -0
- package/dist/core/extensions/session-control-types.d.ts +15 -0
- package/dist/core/extensions/session-control-types.js +11 -0
- package/dist/core/extensions/session-dialog-api.d.ts +5 -0
- package/dist/core/extensions/session-dialog-api.js +13 -0
- package/dist/core/extensions/types.d.ts +14 -1
- package/dist/core/external-admission.d.ts +16 -2
- package/dist/core/external-admission.js +22 -4
- package/dist/core/permission-decision.d.ts +22 -0
- package/dist/core/permission-decision.js +53 -0
- package/dist/core/permission-mode.d.ts +56 -0
- package/dist/core/permission-mode.js +133 -0
- package/dist/core/retry-fallback/controller-types.d.ts +6 -0
- package/dist/core/retry-fallback/controller.d.ts +9 -0
- package/dist/core/retry-fallback/controller.js +34 -3
- package/dist/core/retry-fallback/unusable.d.ts +25 -1
- package/dist/core/retry-fallback/unusable.js +87 -2
- package/dist/core/retry-fallback/usage-limit.d.ts +6 -1
- package/dist/core/retry-fallback/usage-limit.js +12 -4
- package/dist/core/sdk.d.ts +6 -1
- package/dist/core/sdk.js +3 -0
- package/dist/core/session-log.js +1 -1
- package/dist/core/stall-warning.d.ts +41 -0
- package/dist/core/stall-warning.js +72 -0
- package/dist/core/terminal-settings.d.ts +1 -0
- package/dist/core/turn-origin.d.ts +31 -0
- package/dist/core/turn-origin.js +64 -0
- package/dist/modes/interactive/components/remote-delivery-message.d.ts +2 -1
- package/dist/modes/interactive/components/remote-delivery-message.js +13 -5
- package/dist/modes/rpc/admit-external-message-command.d.ts +37 -0
- package/dist/modes/rpc/admit-external-message-command.js +61 -0
- package/dist/modes/rpc/admit-external-message-wire.d.ts +42 -0
- package/dist/modes/rpc/admit-external-message-wire.js +55 -0
- package/dist/modes/rpc/client-input-handler.d.ts +2 -0
- package/dist/modes/rpc/client-input-handler.js +2 -0
- package/dist/modes/rpc/connection-handler.js +30 -5
- package/dist/modes/rpc/connection-roles.d.ts +6 -0
- package/dist/modes/rpc/connection-roles.js +13 -0
- package/dist/modes/rpc/delegated-drive-origin.d.ts +9 -0
- package/dist/modes/rpc/delegated-drive-origin.js +16 -0
- package/dist/modes/rpc/effect-commands.d.ts +7 -1
- package/dist/modes/rpc/host-bare-endpoint-registration.d.ts +7 -0
- package/dist/modes/rpc/host-bare-endpoint-registration.js +35 -0
- package/dist/modes/rpc/host-capabilities.d.ts +2 -0
- package/dist/modes/rpc/host-capabilities.js +3 -0
- package/dist/modes/rpc/host-handover-parked.d.ts +11 -0
- package/dist/modes/rpc/host-handover-parked.js +17 -0
- package/dist/modes/rpc/host-handover-reopen.js +18 -6
- package/dist/modes/rpc/host-handover-resident-writer.d.ts +16 -0
- package/dist/modes/rpc/host-handover-resident-writer.js +57 -0
- package/dist/modes/rpc/host-idle-handover.js +1 -0
- package/dist/modes/rpc/host-warm.d.ts +16 -1
- package/dist/modes/rpc/host-warm.js +33 -6
- package/dist/modes/rpc/method-classes-schema-delivery.d.ts +32 -0
- package/dist/modes/rpc/method-classes-schema-delivery.js +22 -0
- package/dist/modes/rpc/method-classes-schema-session.d.ts +36 -0
- package/dist/modes/rpc/method-classes-schema-session.js +3 -0
- package/dist/modes/rpc/method-classes-schema.d.ts +36 -0
- package/dist/modes/rpc/method-classes.d.ts +2 -0
- package/dist/modes/rpc/method-classes.js +4 -0
- package/dist/modes/rpc/multi-session-host.js +6 -1
- package/dist/modes/rpc/permission-preset-command.d.ts +6 -0
- package/dist/modes/rpc/permission-preset-command.js +27 -0
- package/dist/modes/rpc/rpc-input-validation.js +4 -3
- package/dist/modes/rpc/rpc-mutation-response-types.d.ts +7 -0
- package/dist/modes/rpc/rpc-response-types.d.ts +2 -1
- package/dist/modes/rpc/rpc-session-command-types.d.ts +6 -1
- package/dist/modes/rpc/rpc-session-event-types.d.ts +5 -2
- package/dist/modes/rpc/rpc-session-state-types.d.ts +2 -1
- package/dist/modes/rpc/rpc-session-state.js +2 -0
- package/dist/modes/rpc/session-attachment-commands.js +3 -3
- package/dist/modes/rpc/session-event-writer-connections.js +1 -0
- package/dist/modes/rpc/session-event-writer.d.ts +1 -1
- package/dist/modes/rpc/session-registry-attach.d.ts +0 -9
- package/dist/modes/rpc/session-registry-attach.js +16 -3
- package/dist/modes/rpc/session-registry-types.d.ts +2 -0
- package/dist/modes/rpc/session-registry.d.ts +2 -0
- package/dist/modes/rpc/session-registry.js +16 -3
- package/dist/modes/rpc/session-router-close.js +43 -6
- package/dist/modes/rpc/session-router-context.js +2 -0
- package/dist/modes/rpc/session-router-occupancy.js +2 -11
- package/dist/modes/rpc/session-router-open-profile.js +4 -0
- package/dist/modes/rpc/session-router-open.d.ts +1 -1
- package/dist/modes/rpc/session-router-open.js +6 -17
- package/dist/modes/rpc/session-router-residency.d.ts +24 -0
- package/dist/modes/rpc/session-router-residency.js +90 -0
- package/dist/modes/rpc/session-router-routing.js +7 -1
- package/dist/modes/rpc/session-status.js +4 -1
- package/dist/modes/rpc/session-worker.js +4 -0
- package/dist/modes/rpc/tool-child/terminal-jobs.d.ts +1 -0
- package/dist/modes/rpc/tool-child/terminal-jobs.js +17 -1
- package/dist/modes/rpc/worker-session-registry.js +5 -2
- package/docs/engine-socket/fixtures/admission-routes/h-legacy.json +1 -1
- package/docs/engine-socket/fixtures/rpc-inbound-samples.json +29 -2
- package/docs/extensions.md +12 -0
- package/docs/rpc.md +129 -12
- package/docs/terminal-tools.md +25 -1
- package/native/socket-secret/prebuilds/win32-arm64/senpi_socket_secret.win32-arm64.node +0 -0
- package/native/socket-secret/prebuilds/win32-x64/senpi_socket_secret.win32-x64.node +0 -0
- package/package.json +6 -6
- package/dist/bundle/chunks/chunk-C7HEEBFH.js +0 -2
- package/dist/bundle/chunks/chunk-XCMII2VQ.js +0 -98
- package/dist/bundle/chunks/interactive-mode-B5SQDLH7.js +0 -2
- package/dist/bundle/chunks/main-UQFVFCNY.js +0 -2
- package/dist/bundle/chunks/rpc-mode-JYN3E7P7.js +0 -2
|
@@ -2,17 +2,25 @@ import { describeProviderFailureForUser, isQuotaExhaustionMessage } from "@earen
|
|
|
2
2
|
import { rateLimitModelFamily } from "../credential-pool/model-scope.js";
|
|
3
3
|
import { isBillingErrorMessage } from "./billing.js";
|
|
4
4
|
export class AccountUsageLimitError extends Error {
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
/**
|
|
6
|
+
* `families`: a no-family weekly limit confirmed on these families before it held the provider (senpi#5596).
|
|
7
|
+
* `accountsOut`: a pooled provider's accounts, each with why it cannot serve (senpi#5590).
|
|
8
|
+
*/
|
|
9
|
+
constructor(provider, raw = "usage limit reached", resetsAt, families, accountsOut) {
|
|
10
|
+
const message = describeProviderFailureForUser(raw, {
|
|
7
11
|
model: provider,
|
|
8
12
|
usageLimit: "account",
|
|
9
13
|
...(resetsAt === undefined ? {} : { resetsAt: formatResetTime(resetsAt) }),
|
|
10
|
-
})
|
|
14
|
+
});
|
|
15
|
+
const withFamilies = families === undefined || families.length === 0
|
|
16
|
+
? message
|
|
17
|
+
: `${message} The limit named no model family; it was confirmed on ${families.join(" and ")}.`;
|
|
18
|
+
super(accountsOut === undefined ? withFamilies : `${withFamilies} No account can serve: ${accountsOut}.`);
|
|
11
19
|
this.name = "AccountUsageLimitError";
|
|
12
20
|
}
|
|
13
21
|
}
|
|
14
22
|
// The same reader-local time plus timezone the credential pool's blocked message uses.
|
|
15
|
-
function formatResetTime(epochMs) {
|
|
23
|
+
export function formatResetTime(epochMs) {
|
|
16
24
|
return new Date(epochMs).toLocaleString("en-US", {
|
|
17
25
|
month: "short",
|
|
18
26
|
day: "numeric",
|
package/dist/core/sdk.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
1
|
+
import { type AgentOptions, type ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
2
2
|
import type { ThinkingSelection } from "@earendil-works/pi-ai";
|
|
3
3
|
import { type Model } from "@earendil-works/pi-ai/compat";
|
|
4
4
|
import { AgentSession } from "./agent-session.ts";
|
|
@@ -17,6 +17,11 @@ import { createBashTool, createCodingTools, createEditTool, createFindTool, crea
|
|
|
17
17
|
import type { ToolRuntimeKind } from "../cli/tool-runtime.ts";
|
|
18
18
|
import { type ExecutionPipelineWitnessSink } from "./execution/pipeline-witness-sink.ts";
|
|
19
19
|
export interface CreateAgentSessionOptions {
|
|
20
|
+
/**
|
|
21
|
+
* Makes each finished model step durable before the loop acts on it (hosted custody, C6 C.P3). Passed to the Agent
|
|
22
|
+
* this function builds; fused and split-local sessions leave it unset.
|
|
23
|
+
*/
|
|
24
|
+
commitStep?: AgentOptions["commitStep"];
|
|
20
25
|
/**
|
|
21
26
|
* Where this session's execution-pipeline receipts go (S18b part 3): a sink outside the session, never a session entry.
|
|
22
27
|
* Unset: the file `SENPI_EXECUTION_PIPELINE_WITNESS` names (one JSON line per call), else no receipts.
|
package/dist/core/sdk.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
+
import { bindPermissionDecision } from "./permission-mode.js";
|
|
2
3
|
import { Agent, createInternalAgentChannel, setDefaultStreamFn, } from "@earendil-works/pi-agent-core";
|
|
3
4
|
import { modelsAreEqual, streamSimple } from "@earendil-works/pi-ai/compat";
|
|
4
5
|
import { getAgentDir } from "../config.js";
|
|
@@ -291,6 +292,7 @@ export async function createAgentSession(options = {}) {
|
|
|
291
292
|
agent = new Agent({
|
|
292
293
|
internalChannel,
|
|
293
294
|
onExecutionPipeline: resolveWitnessSink(options.executionPipelineWitness),
|
|
295
|
+
...(options.commitStep ? { commitStep: options.commitStep } : {}),
|
|
294
296
|
initialState: {
|
|
295
297
|
systemPrompt: "",
|
|
296
298
|
model,
|
|
@@ -486,6 +488,7 @@ export async function createAgentSession(options = {}) {
|
|
|
486
488
|
sessionManager.appendThinkingLevelChange(thinkingLevel, thinkingSelection, undefined, model);
|
|
487
489
|
}
|
|
488
490
|
sessionRef.current = session;
|
|
491
|
+
bindPermissionDecision(session);
|
|
489
492
|
const extensionsResult = resourceLoader.getExtensions();
|
|
490
493
|
return {
|
|
491
494
|
session,
|
package/dist/core/session-log.js
CHANGED
|
@@ -7,7 +7,7 @@ const DEFAULT_MAX_BYTES = 5 * 1024 * 1024;
|
|
|
7
7
|
const MAX_STRING_LENGTH = 200;
|
|
8
8
|
const DEBUG_PREFIX = `[${APP_NAME}-session]`;
|
|
9
9
|
const BLOCKED_KEY = /^(?:__proto__|constructor|prototype|headers?|env(?:ironment)?|authorization|credential(?:s)?|password|secret|token|api_?key|client_?secret)$/i;
|
|
10
|
-
const ALLOWED_DATA_KEY = /^(?:action|attemptId|disposition|stage|error|mode|count|willRetry|deferAdmission|delivered|restored|cause|accepted|skipped|rejectionCause|reason|durationMs|kind|retryable|phase|op|bytes|generation|requestId|tokens|tokensBefore|tokensAfter|contextWindow|attempt|aborted|sessionId|role|provider|model|source|actor|from|to|duringTurn|persistDefault|extension|handlerEvent|queuedId|ageMs|seat|until|shared)$/;
|
|
10
|
+
const ALLOWED_DATA_KEY = /^(?:action|attemptId|disposition|stage|error|mode|count|willRetry|deferAdmission|delivered|restored|cause|accepted|skipped|rejectionCause|reason|durationMs|kind|retryable|phase|op|bytes|generation|requestId|tokens|tokensBefore|tokensAfter|contextWindow|attempt|aborted|sessionId|role|provider|model|source|actor|from|to|duringTurn|persistDefault|extension|handlerEvent|queuedId|ageMs|seat|until|shared|toolCallId|parentToolCallId|toolName|thresholdMs|elapsedMs|queueDepth|queueHead)$/;
|
|
11
11
|
const SENSITIVE_TEXT = /((?:authorization\s*[:=]\s*(?:bearer|basic)\s+)|(?:bearer\s+)|(?:[?&](?:api[_-]?key|token|secret|password|auth(?:orization)?)=))[^\s&,"'}\]]+/gi;
|
|
12
12
|
export function createSessionLogger(agentDir, options = {}) {
|
|
13
13
|
if (typeof agentDir !== "string" || agentDir.length === 0) {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deadline-bounded warnings for waits that have no deadline of their own (senpi#6189).
|
|
3
|
+
*
|
|
4
|
+
* A wait that never settles leaves no trace: no timer fires, no result is written, and the
|
|
5
|
+
* session just idles. These helpers only observe such a wait. They log once when it outlives
|
|
6
|
+
* its threshold and never resolve, reject, abort or delay anything themselves.
|
|
7
|
+
*/
|
|
8
|
+
/** A wait still pending after this long is logged once. A bash spawn's own shorter timeout lowers it. */
|
|
9
|
+
export declare const STALL_WARNING_MS = 30000;
|
|
10
|
+
/** The warning threshold for a bash spawn whose own `timeout` (in seconds) may be shorter than {@link STALL_WARNING_MS}. */
|
|
11
|
+
export declare function stallThresholdMs(timeoutSeconds: unknown): number;
|
|
12
|
+
export declare function queueEntryLabel(event: {
|
|
13
|
+
readonly type: string;
|
|
14
|
+
}): string;
|
|
15
|
+
export interface StallWatch {
|
|
16
|
+
stop(): void;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Call `onStall` once if `stop` has not run within `thresholdMs`. The caller keeps its own `await`, so the
|
|
20
|
+
* watched wait's outcome and ordering are unchanged. The timer is unref'd: it never keeps the process alive.
|
|
21
|
+
*/
|
|
22
|
+
export declare function watchForStall(thresholdMs: number, onStall: (elapsedMs: number) => void): StallWatch;
|
|
23
|
+
export interface PendingQueueSnapshot {
|
|
24
|
+
readonly queueDepth: number;
|
|
25
|
+
/** Label of the oldest unsettled entry: the one every later entry waits behind. */
|
|
26
|
+
readonly queueHead?: string;
|
|
27
|
+
readonly ageMs?: number;
|
|
28
|
+
}
|
|
29
|
+
/** The unsettled entries of a promise chain, oldest first, so a stalled wait on the chain can name what it waits behind. */
|
|
30
|
+
export declare class PendingQueueLedger {
|
|
31
|
+
private nextId;
|
|
32
|
+
private readonly entries;
|
|
33
|
+
/**
|
|
34
|
+
* Record `run` as an entry from now until it settles, and return it wrapped for the chain. The wrapper returns
|
|
35
|
+
* exactly what `run` returns or throws, and releases the entry from that result. It never attaches a handler to
|
|
36
|
+
* the chain itself, so a rejection on the chain is exactly as visible (or unhandled) as before.
|
|
37
|
+
*/
|
|
38
|
+
track<A extends unknown[], R>(label: string, run: (...args: A) => R): (...args: A) => R;
|
|
39
|
+
snapshot(): PendingQueueSnapshot;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=stall-warning.d.ts.map
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deadline-bounded warnings for waits that have no deadline of their own (senpi#6189).
|
|
3
|
+
*
|
|
4
|
+
* A wait that never settles leaves no trace: no timer fires, no result is written, and the
|
|
5
|
+
* session just idles. These helpers only observe such a wait. They log once when it outlives
|
|
6
|
+
* its threshold and never resolve, reject, abort or delay anything themselves.
|
|
7
|
+
*/
|
|
8
|
+
/** A wait still pending after this long is logged once. A bash spawn's own shorter timeout lowers it. */
|
|
9
|
+
export const STALL_WARNING_MS = 30_000;
|
|
10
|
+
/** The warning threshold for a bash spawn whose own `timeout` (in seconds) may be shorter than {@link STALL_WARNING_MS}. */
|
|
11
|
+
export function stallThresholdMs(timeoutSeconds) {
|
|
12
|
+
if (typeof timeoutSeconds !== "number" || !Number.isFinite(timeoutSeconds) || timeoutSeconds <= 0) {
|
|
13
|
+
return STALL_WARNING_MS;
|
|
14
|
+
}
|
|
15
|
+
return Math.max(1, Math.min(STALL_WARNING_MS, Math.trunc(timeoutSeconds * 1000)));
|
|
16
|
+
}
|
|
17
|
+
export function queueEntryLabel(event) {
|
|
18
|
+
const toolCallId = event.toolCallId;
|
|
19
|
+
return typeof toolCallId === "string" ? `${event.type}:${toolCallId}` : event.type;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Call `onStall` once if `stop` has not run within `thresholdMs`. The caller keeps its own `await`, so the
|
|
23
|
+
* watched wait's outcome and ordering are unchanged. The timer is unref'd: it never keeps the process alive.
|
|
24
|
+
*/
|
|
25
|
+
export function watchForStall(thresholdMs, onStall) {
|
|
26
|
+
const startedAt = Date.now();
|
|
27
|
+
const timer = setTimeout(() => onStall(Date.now() - startedAt), thresholdMs);
|
|
28
|
+
timer.unref?.();
|
|
29
|
+
return { stop: () => clearTimeout(timer) };
|
|
30
|
+
}
|
|
31
|
+
/** The unsettled entries of a promise chain, oldest first, so a stalled wait on the chain can name what it waits behind. */
|
|
32
|
+
export class PendingQueueLedger {
|
|
33
|
+
constructor() {
|
|
34
|
+
this.nextId = 0;
|
|
35
|
+
this.entries = new Map();
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Record `run` as an entry from now until it settles, and return it wrapped for the chain. The wrapper returns
|
|
39
|
+
* exactly what `run` returns or throws, and releases the entry from that result. It never attaches a handler to
|
|
40
|
+
* the chain itself, so a rejection on the chain is exactly as visible (or unhandled) as before.
|
|
41
|
+
*/
|
|
42
|
+
track(label, run) {
|
|
43
|
+
const id = this.nextId++;
|
|
44
|
+
this.entries.set(id, { label, enqueuedAt: Date.now() });
|
|
45
|
+
const release = () => {
|
|
46
|
+
this.entries.delete(id);
|
|
47
|
+
};
|
|
48
|
+
return (...args) => {
|
|
49
|
+
let result;
|
|
50
|
+
try {
|
|
51
|
+
result = run(...args);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
release();
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
// The chain adopts `result`, which marks it handled anyway; this handler changes nobody's view of it.
|
|
58
|
+
if (result instanceof Promise)
|
|
59
|
+
void result.then(release, release);
|
|
60
|
+
else
|
|
61
|
+
release();
|
|
62
|
+
return result;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
snapshot() {
|
|
66
|
+
const head = this.entries.values().next().value;
|
|
67
|
+
if (head === undefined)
|
|
68
|
+
return { queueDepth: 0 };
|
|
69
|
+
return { queueDepth: this.entries.size, queueHead: head.label, ageMs: Date.now() - head.enqueuedAt };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=stall-warning.js.map
|
|
@@ -25,6 +25,7 @@ export interface TerminalSettings {
|
|
|
25
25
|
monitorRunawayLinesPerMinute?: number;
|
|
26
26
|
monitorRunawayMinutes?: number;
|
|
27
27
|
maxDurableMonitors?: number | "unlimited";
|
|
28
|
+
backgroundAutoReapMinutes?: number;
|
|
28
29
|
killGuard?: "on" | "off";
|
|
29
30
|
}
|
|
30
31
|
export interface BranchSummarySettings {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { SessionManager } from "./session-manager.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Who started a turn, for the delegated-default permission rule (remote approval-mode work):
|
|
4
|
+
* - `local`: a local connection's or the TUI's own input;
|
|
5
|
+
* - `delegated`: input that arrived on a delegated (remote device) connection;
|
|
6
|
+
* - `engine`: a turn no client record started (an extension's message, a goal or loop turn, restored input).
|
|
7
|
+
* A steer joins the running turn (`join`) and never changes its origin.
|
|
8
|
+
*/
|
|
9
|
+
export type TurnOrigin = "local" | "delegated" | "engine";
|
|
10
|
+
export type InputTurnMark = TurnOrigin | "join";
|
|
11
|
+
export type ConnectionOrigin = "local" | "delegated";
|
|
12
|
+
type OriginSessionManager = Pick<SessionManager, "getEntries" | "appendCustomEntry">;
|
|
13
|
+
/** The mark an input gets where it is created: explicit for client records, `local` for the TUI, else `engine`. */
|
|
14
|
+
export declare function inputTurnMark(input: {
|
|
15
|
+
readonly mode?: "steer" | "followUp";
|
|
16
|
+
readonly connectionOrigin?: ConnectionOrigin;
|
|
17
|
+
readonly source?: string;
|
|
18
|
+
}): InputTurnMark;
|
|
19
|
+
export declare function markInputTurnOrigin(message: object, mark: InputTurnMark): void;
|
|
20
|
+
/** Carries `from`'s origin mark onto `to`, a copy of the same input: a copy without it would open or join a turn unmarked. */
|
|
21
|
+
export declare function copyInputTurnMark(from: object, to: object): void;
|
|
22
|
+
/** Whether a message carries an origin mark: a user message always opens a turn's origin, any other only when marked. */
|
|
23
|
+
export declare function hasInputTurnMark(message: object): boolean;
|
|
24
|
+
/** A run starts as engine work; the user message that opens a turn sets its origin, a steer keeps it. */
|
|
25
|
+
export declare function noteRunStart(sessionManager: object): void;
|
|
26
|
+
export declare function noteUserMessageStart(sessionManager: object, message: object): void;
|
|
27
|
+
export declare function recordDelegatedOrigin(sessionManager: OriginSessionManager): void;
|
|
28
|
+
/** Whether the deciding turn counts as delegated-origin: a remote device started it, or nobody did on a remote-driven session. */
|
|
29
|
+
export declare function decidingTurnIsDelegated(sessionManager: Pick<SessionManager, "getEntries">): boolean;
|
|
30
|
+
export {};
|
|
31
|
+
//# sourceMappingURL=turn-origin.d.ts.map
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const DELEGATED_ORIGIN_ENTRY = "delegated_origin";
|
|
2
|
+
const inputMarks = new WeakMap();
|
|
3
|
+
const currentOrigins = new WeakMap();
|
|
4
|
+
const delegatedSessions = new WeakSet();
|
|
5
|
+
/** The mark an input gets where it is created: explicit for client records, `local` for the TUI, else `engine`. */
|
|
6
|
+
export function inputTurnMark(input) {
|
|
7
|
+
if (input.mode === "steer")
|
|
8
|
+
return "join";
|
|
9
|
+
if (input.connectionOrigin !== undefined)
|
|
10
|
+
return input.connectionOrigin;
|
|
11
|
+
return input.source === undefined || input.source === "interactive" ? "local" : "engine";
|
|
12
|
+
}
|
|
13
|
+
export function markInputTurnOrigin(message, mark) {
|
|
14
|
+
inputMarks.set(message, mark);
|
|
15
|
+
}
|
|
16
|
+
/** Carries `from`'s origin mark onto `to`, a copy of the same input: a copy without it would open or join a turn unmarked. */
|
|
17
|
+
export function copyInputTurnMark(from, to) {
|
|
18
|
+
const mark = inputMarks.get(from);
|
|
19
|
+
if (mark !== undefined)
|
|
20
|
+
inputMarks.set(to, mark);
|
|
21
|
+
}
|
|
22
|
+
/** Whether a message carries an origin mark: a user message always opens a turn's origin, any other only when marked. */
|
|
23
|
+
export function hasInputTurnMark(message) {
|
|
24
|
+
return inputMarks.has(message);
|
|
25
|
+
}
|
|
26
|
+
/** A run starts as engine work; the user message that opens a turn sets its origin, a steer keeps it. */
|
|
27
|
+
export function noteRunStart(sessionManager) {
|
|
28
|
+
currentOrigins.set(sessionManager, "engine");
|
|
29
|
+
}
|
|
30
|
+
export function noteUserMessageStart(sessionManager, message) {
|
|
31
|
+
const mark = inputMarks.get(message);
|
|
32
|
+
if (mark === "join")
|
|
33
|
+
return;
|
|
34
|
+
currentOrigins.set(sessionManager, mark ?? "engine");
|
|
35
|
+
}
|
|
36
|
+
export function recordDelegatedOrigin(sessionManager) {
|
|
37
|
+
if (delegatedOriginOf(sessionManager))
|
|
38
|
+
return;
|
|
39
|
+
delegatedSessions.add(sessionManager);
|
|
40
|
+
sessionManager.appendCustomEntry(DELEGATED_ORIGIN_ENTRY, { delegatedOrigin: true }, { startupBookkeeping: true });
|
|
41
|
+
}
|
|
42
|
+
// How many entries a session's scan has already found free of the marker: each gated decision reads only new entries.
|
|
43
|
+
const scannedEntries = new WeakMap();
|
|
44
|
+
function delegatedOriginOf(sessionManager) {
|
|
45
|
+
if (delegatedSessions.has(sessionManager))
|
|
46
|
+
return true;
|
|
47
|
+
const entries = sessionManager.getEntries();
|
|
48
|
+
const persisted = entries
|
|
49
|
+
.slice(scannedEntries.get(sessionManager) ?? 0)
|
|
50
|
+
.some((entry) => entry.type === "custom" && entry.customType === DELEGATED_ORIGIN_ENTRY);
|
|
51
|
+
if (persisted)
|
|
52
|
+
delegatedSessions.add(sessionManager);
|
|
53
|
+
else
|
|
54
|
+
scannedEntries.set(sessionManager, entries.length);
|
|
55
|
+
return persisted;
|
|
56
|
+
}
|
|
57
|
+
/** Whether the deciding turn counts as delegated-origin: a remote device started it, or nobody did on a remote-driven session. */
|
|
58
|
+
export function decidingTurnIsDelegated(sessionManager) {
|
|
59
|
+
const origin = currentOrigins.get(sessionManager) ?? "engine";
|
|
60
|
+
if (origin === "delegated")
|
|
61
|
+
return true;
|
|
62
|
+
return origin === "engine" && delegatedOriginOf(sessionManager);
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=turn-origin.js.map
|
|
@@ -3,7 +3,8 @@ import { type MessageRenderer, type SessionControlSender } from "../../../core/e
|
|
|
3
3
|
* A message another session delivered renders as its own block - never as the user's own input, so
|
|
4
4
|
* a reader always sees which text came from outside this terminal. A delivery that names its sender
|
|
5
5
|
* shows one label line ("Sent by another agent · <name>") over the message as it was written; one
|
|
6
|
-
* that does not keeps the generic heading over the text the model read.
|
|
6
|
+
* that does not keeps the generic heading over the text the model read. A delivery an RPC connection admitted for a
|
|
7
|
+
* remote device leads with the device the engine stamped, before the sender that device's caller claimed.
|
|
7
8
|
*/
|
|
8
9
|
export declare const renderRemoteDelivery: MessageRenderer;
|
|
9
10
|
export declare function senderLabel(sender: SessionControlSender): string;
|
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
import { Box, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
|
|
2
|
-
import { isSessionControlDeliveryDetails, SESSION_CONTROL_DELIVERY_TYPE, sessionControlSenderOf, } from "../../../core/extensions/types.js";
|
|
2
|
+
import { isSessionControlDeliveryDetails, SESSION_CONTROL_DELIVERY_TYPE, sessionControlRemoteOriginOf, sessionControlSenderOf, } from "../../../core/extensions/types.js";
|
|
3
3
|
import { sharedMarkdownTheme } from "../theme/theme.js";
|
|
4
4
|
/**
|
|
5
5
|
* A message another session delivered renders as its own block - never as the user's own input, so
|
|
6
6
|
* a reader always sees which text came from outside this terminal. A delivery that names its sender
|
|
7
7
|
* shows one label line ("Sent by another agent · <name>") over the message as it was written; one
|
|
8
|
-
* that does not keeps the generic heading over the text the model read.
|
|
8
|
+
* that does not keeps the generic heading over the text the model read. A delivery an RPC connection admitted for a
|
|
9
|
+
* remote device leads with the device the engine stamped, before the sender that device's caller claimed.
|
|
9
10
|
*/
|
|
10
11
|
export const renderRemoteDelivery = (message, _options, theme) => {
|
|
11
12
|
const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
|
|
12
13
|
const sender = sessionControlSenderOf(message.details);
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
const origin = sessionControlRemoteOriginOf(message.details);
|
|
15
|
+
const shown = sender === undefined && origin === undefined ? undefined : displayTextOf(message.details);
|
|
16
|
+
if (shown !== undefined) {
|
|
17
|
+
if (origin !== undefined)
|
|
18
|
+
box.addChild(new Text(theme.fg("dim", originLabel(origin)), 0, 0));
|
|
19
|
+
if (sender !== undefined)
|
|
20
|
+
box.addChild(new Text(theme.fg("dim", senderLabel(sender)), 0, 0));
|
|
16
21
|
box.addChild(new Spacer(1));
|
|
17
22
|
box.addChild(new Markdown(shown, 0, 0, sharedMarkdownTheme(), { color: (line) => theme.fg("customMessageText", line) }));
|
|
18
23
|
return box;
|
|
@@ -43,6 +48,9 @@ export function senderLabel(sender) {
|
|
|
43
48
|
: `Sent from ${sender.platform} · ${sender.author}`;
|
|
44
49
|
}
|
|
45
50
|
}
|
|
51
|
+
function originLabel(origin) {
|
|
52
|
+
return `Relayed from device ${origin.deviceName ?? origin.deviceId}`;
|
|
53
|
+
}
|
|
46
54
|
export function builtInMessageRenderer(customType) {
|
|
47
55
|
return customType === SESSION_CONTROL_DELIVERY_TYPE ? renderRemoteDelivery : undefined;
|
|
48
56
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `admit_external_message` (senpi#6233): the session's own `ExternalAdmission.admit`, the call behind
|
|
3
|
+
* `pi.session.admitExternalMessage`. The engine writes its fixed `session_control_delivery` entry: never a user message,
|
|
4
|
+
* never the extension `input` event, never a slash command or prompt template, whatever the text says.
|
|
5
|
+
*
|
|
6
|
+
* What the engine adds comes from the connection the record arrived on (`DeliveryPeer`), never from the record (the
|
|
7
|
+
* security design gate's binding conditions): the device a remote connection speaks for is stamped into the entry's
|
|
8
|
+
* metadata (`details.remote_origin`) while the model reads exactly the text a same-machine delivery carries (senpi#6250:
|
|
9
|
+
* an agent need not know a message crossed devices), the ledger keys the delivery under that device, a `command_line` sender is refused
|
|
10
|
+
* from a remote device, the turn that consumes the delivery takes the connection's origin, the thread-chain guard
|
|
11
|
+
* runs first, and an id the transcript already holds is answered `already_admitted` after a restart too.
|
|
12
|
+
*/
|
|
13
|
+
import type { AgentSession } from "../../core/agent-session.ts";
|
|
14
|
+
import type { ConnectionOrigin } from "../../core/turn-origin.ts";
|
|
15
|
+
import type { RpcAdmitExternalMessageCommand, RpcAdmitExternalMessageReply } from "./admit-external-message-wire.ts";
|
|
16
|
+
import { type ChainRefusal } from "./chain-admission.ts";
|
|
17
|
+
import type { RpcRemoteOrigin } from "./remote-origin.ts";
|
|
18
|
+
/** The connection a delivery arrived on, as the host knows it. */
|
|
19
|
+
export interface DeliveryPeer {
|
|
20
|
+
/** The remote device the connection speaks for (`ConnectionRoles.originOf`); absent on a plain local connection. */
|
|
21
|
+
readonly origin: RpcRemoteOrigin | undefined;
|
|
22
|
+
readonly connectionOrigin: ConnectionOrigin;
|
|
23
|
+
}
|
|
24
|
+
export type AdmitExternalMessageAnswer = {
|
|
25
|
+
readonly ok: true;
|
|
26
|
+
readonly data: RpcAdmitExternalMessageReply;
|
|
27
|
+
} | {
|
|
28
|
+
readonly ok: false;
|
|
29
|
+
readonly refusal: ChainRefusal | {
|
|
30
|
+
readonly message: string;
|
|
31
|
+
readonly errorCode: string;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
type AdmittingSession = Pick<AgentSession, "externalAdmission" | "sessionManager" | "settingsManager">;
|
|
35
|
+
export declare function admitExternalMessage(session: AdmittingSession, command: RpcAdmitExternalMessageCommand, peer: DeliveryPeer): AdmitExternalMessageAnswer;
|
|
36
|
+
export {};
|
|
37
|
+
//# sourceMappingURL=admit-external-message-command.d.ts.map
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { writtenDeliveryIds } from "../../core/external-admission.js";
|
|
2
|
+
import { admitInputChain } from "./chain-admission.js";
|
|
3
|
+
const SENDER_NOT_ALLOWED = "sender_not_allowed";
|
|
4
|
+
const DELEGATED_ORIGIN_UNKNOWN = "delegated_origin_unknown";
|
|
5
|
+
const CANONICAL_TURN_ID = /^(0|[1-9]\d{0,15})$/;
|
|
6
|
+
/** The ledger key: a remote device's ids live under that device, a local connection's under its own prefix. */
|
|
7
|
+
function ledgerId(deliveryId, origin) {
|
|
8
|
+
return origin === undefined
|
|
9
|
+
? `rpc:local/${deliveryId}`
|
|
10
|
+
: `device:${encodeURIComponent(origin.deviceId)}/${deliveryId}`;
|
|
11
|
+
}
|
|
12
|
+
export function admitExternalMessage(session, command, peer) {
|
|
13
|
+
const chain = admitInputChain(command.chainCount, session.settingsManager);
|
|
14
|
+
if (chain.kind === "refused")
|
|
15
|
+
return { ok: false, refusal: chain.refusal };
|
|
16
|
+
const { origin } = peer;
|
|
17
|
+
// A delegated connection that names no device is never treated as this machine's own: it is refused (fail closed).
|
|
18
|
+
if (peer.connectionOrigin === "delegated" && origin === undefined)
|
|
19
|
+
return {
|
|
20
|
+
ok: false,
|
|
21
|
+
refusal: {
|
|
22
|
+
message: `${DELEGATED_ORIGIN_UNKNOWN}: a delegated connection that names no remote device cannot admit a delivery`,
|
|
23
|
+
errorCode: DELEGATED_ORIGIN_UNKNOWN,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
if (origin !== undefined && command.sender?.kind === "command_line")
|
|
27
|
+
return {
|
|
28
|
+
ok: false,
|
|
29
|
+
refusal: {
|
|
30
|
+
message: `${SENDER_NOT_ALLOWED}: a remote device cannot send as this machine's command line`,
|
|
31
|
+
errorCode: SENDER_NOT_ALLOWED,
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
const admission = session.externalAdmission;
|
|
35
|
+
const answer = (kind, epoch) => ({
|
|
36
|
+
ok: true,
|
|
37
|
+
data: { kind, turnId: String(epoch) },
|
|
38
|
+
});
|
|
39
|
+
const id = ledgerId(command.deliveryId, origin);
|
|
40
|
+
if (session.sessionManager.reduceHistory(writtenDeliveryIds).has(id))
|
|
41
|
+
return answer("already_admitted", admission.turnEpoch);
|
|
42
|
+
const expected = command.expectedTurnId;
|
|
43
|
+
if (expected !== undefined && !CANONICAL_TURN_ID.test(expected))
|
|
44
|
+
return answer("turn_conflict", admission.turnEpoch);
|
|
45
|
+
const displayText = command.displayText ?? (origin === undefined ? undefined : command.text);
|
|
46
|
+
const result = admission.admit({
|
|
47
|
+
delivery_id: id,
|
|
48
|
+
text: command.text,
|
|
49
|
+
deliverAs: command.deliverAs,
|
|
50
|
+
...(command.sender === undefined ? {} : { sender: command.sender }),
|
|
51
|
+
...(displayText === undefined ? {} : { display_text: displayText }),
|
|
52
|
+
...(expected === undefined ? {} : { expected_turn_id: Number(expected) }),
|
|
53
|
+
...(command.whenIdle === undefined ? {} : { whenIdle: command.whenIdle }),
|
|
54
|
+
}, {
|
|
55
|
+
...(origin === undefined ? {} : { remoteOrigin: origin }),
|
|
56
|
+
...(chain.chainCount === undefined ? {} : { chainCount: chain.chainCount }),
|
|
57
|
+
turnMark: command.deliverAs === "steer" ? "join" : peer.connectionOrigin,
|
|
58
|
+
});
|
|
59
|
+
return answer(result.kind, result.turn_epoch);
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=admit-external-message-command.js.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wire of `admit_external_message` (senpi#6233): a message another agent sent this session, relayed over RPC and
|
|
3
|
+
* admitted the way `pi.session.admitExternalMessage` admits it in process. The fields mirror
|
|
4
|
+
* `AdmitExternalMessageInput` in the wire's camelCase, plus a thread call's `chainCount`; turn ids are strings, as
|
|
5
|
+
* `get_state.currentTurnId`. The field checks run on the local connection and inside `validateRpcInboundRecord`, so
|
|
6
|
+
* both paths refuse the same records.
|
|
7
|
+
*/
|
|
8
|
+
import type { ExternalAdmissionKind, ExternalDeliverAs, SessionControlSender } from "../../core/extensions/session-control-types.ts";
|
|
9
|
+
export interface RpcAdmitExternalMessageCommand {
|
|
10
|
+
id?: string;
|
|
11
|
+
type: "admit_external_message";
|
|
12
|
+
/** The delivery's id: admitting an id this runtime already holds or wrote answers `already_admitted`. */
|
|
13
|
+
deliveryId: string;
|
|
14
|
+
/** What the model reads, provenance included. */
|
|
15
|
+
text: string;
|
|
16
|
+
/** Who the caller says sent it, for the surfaces a human reads; a claim, shown after the engine-stamped device. */
|
|
17
|
+
sender?: SessionControlSender;
|
|
18
|
+
/** The message as its sender wrote it, for human surfaces. */
|
|
19
|
+
displayText?: string;
|
|
20
|
+
deliverAs: ExternalDeliverAs;
|
|
21
|
+
/** The turn the sender observed (`get_state.currentTurnId`); a steer is admitted only while it is current. */
|
|
22
|
+
expectedTurnId?: string;
|
|
23
|
+
/** What an idle session does with it: `start` (the default) starts a turn, `queue` holds it for the next one. */
|
|
24
|
+
whenIdle?: "start" | "queue";
|
|
25
|
+
/** A thread call's hop count (1 for a chain's first hop), checked against the user's chain limit first (K8). */
|
|
26
|
+
chainCount?: number;
|
|
27
|
+
}
|
|
28
|
+
export interface RpcAdmitExternalMessageReply {
|
|
29
|
+
kind: ExternalAdmissionKind;
|
|
30
|
+
/** The session's turn when the delivery was admitted, in `get_state.currentTurnId`'s form. */
|
|
31
|
+
turnId: string;
|
|
32
|
+
}
|
|
33
|
+
export interface RpcAdmitExternalMessageResponse {
|
|
34
|
+
id?: string;
|
|
35
|
+
type: "response";
|
|
36
|
+
command: "admit_external_message";
|
|
37
|
+
success: true;
|
|
38
|
+
data: RpcAdmitExternalMessageReply;
|
|
39
|
+
}
|
|
40
|
+
/** Why an `admit_external_message` record is refused, prefixed with its type; `undefined` for any other record. */
|
|
41
|
+
export declare function admitExternalMessageFieldError(value: Record<string, unknown>, maxTextCharacters: number): string | undefined;
|
|
42
|
+
//# sourceMappingURL=admit-external-message-wire.d.ts.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/** The bound every id and label of the record stays inside, as the other local field checks bound theirs. */
|
|
2
|
+
const MAX_ID_CHARACTERS = 256;
|
|
3
|
+
const SENDER_FIELDS = {
|
|
4
|
+
agent: { required: ["session_id"], optional: ["name"] },
|
|
5
|
+
command_line: { required: [], optional: ["user"] },
|
|
6
|
+
external: { required: ["platform"], optional: ["author"] },
|
|
7
|
+
};
|
|
8
|
+
const SENDER_SHAPE = 'sender must be { kind: "agent", session_id, name? }, { kind: "command_line", user? } or { kind: "external", platform, author? }, each value a non-empty string of at most 256 characters.';
|
|
9
|
+
function boundedId(value) {
|
|
10
|
+
return typeof value === "string" && value.length > 0 && value.length <= MAX_ID_CHARACTERS;
|
|
11
|
+
}
|
|
12
|
+
/** A well-formed sender of a known kind with no other field, so nothing beyond the label can ride along. */
|
|
13
|
+
function isDeliverySender(value) {
|
|
14
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
15
|
+
return false;
|
|
16
|
+
const kind = Reflect.get(value, "kind");
|
|
17
|
+
if (typeof kind !== "string" || !Object.hasOwn(SENDER_FIELDS, kind))
|
|
18
|
+
return false;
|
|
19
|
+
const { required, optional } = SENDER_FIELDS[kind];
|
|
20
|
+
const allowed = new Set(["kind", ...required, ...optional]);
|
|
21
|
+
return (Object.keys(value).every((key) => allowed.has(key)) &&
|
|
22
|
+
required.every((key) => boundedId(Reflect.get(value, key))) &&
|
|
23
|
+
optional.every((key) => Reflect.get(value, key) === undefined || boundedId(Reflect.get(value, key))));
|
|
24
|
+
}
|
|
25
|
+
function fieldProblem(value, maxTextCharacters) {
|
|
26
|
+
if (!boundedId(value.deliveryId))
|
|
27
|
+
return "needs deliveryId, a non-empty string of at most 256 characters.";
|
|
28
|
+
if (typeof value.text !== "string" || value.text.trim() === "" || value.text.length > maxTextCharacters)
|
|
29
|
+
return `needs text, a non-empty string of at most ${maxTextCharacters} characters.`;
|
|
30
|
+
if (value.deliverAs !== "steer" && value.deliverAs !== "followUp")
|
|
31
|
+
return 'deliverAs must be "steer" or "followUp".';
|
|
32
|
+
if (value.displayText !== undefined &&
|
|
33
|
+
(typeof value.displayText !== "string" || value.displayText.length > maxTextCharacters))
|
|
34
|
+
return `displayText must be a string of at most ${maxTextCharacters} characters.`;
|
|
35
|
+
if (value.expectedTurnId !== undefined && !boundedId(value.expectedTurnId))
|
|
36
|
+
return "expectedTurnId must be a non-empty string of at most 256 characters; read it from get_state.currentTurnId.";
|
|
37
|
+
if (value.chainCount !== undefined && !(Number.isSafeInteger(value.chainCount) && value.chainCount >= 1))
|
|
38
|
+
return "chainCount must be a positive integer (1 for a chain's first hop).";
|
|
39
|
+
// The device is stamped by the engine from the connection; a record that names one is refused, never copied.
|
|
40
|
+
if (Object.hasOwn(value, "remote_origin") || Object.hasOwn(value, "remoteOrigin"))
|
|
41
|
+
return "must not name remote_origin: the engine stamps the device the connection speaks for.";
|
|
42
|
+
if (value.whenIdle !== undefined && value.whenIdle !== "start" && value.whenIdle !== "queue")
|
|
43
|
+
return 'whenIdle must be "start" or "queue"; omit it to start a turn on an idle session.';
|
|
44
|
+
if (value.sender !== undefined && !isDeliverySender(value.sender))
|
|
45
|
+
return SENDER_SHAPE;
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
/** Why an `admit_external_message` record is refused, prefixed with its type; `undefined` for any other record. */
|
|
49
|
+
export function admitExternalMessageFieldError(value, maxTextCharacters) {
|
|
50
|
+
if (value.type !== "admit_external_message")
|
|
51
|
+
return undefined;
|
|
52
|
+
const problem = fieldProblem(value, maxTextCharacters);
|
|
53
|
+
return problem === undefined ? undefined : `admit_external_message ${problem}`;
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=admit-external-message-wire.js.map
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AgentSession } from "../../core/agent-session.ts";
|
|
2
|
+
import type { ConnectionOrigin } from "../../core/turn-origin.ts";
|
|
2
3
|
import { type RpcClientInput } from "./client-admission-record.ts";
|
|
3
4
|
type InputOutput = {
|
|
4
5
|
readonly output: (record: object) => void;
|
|
@@ -7,6 +8,7 @@ type InputOutput = {
|
|
|
7
8
|
/** The sending connection's declared role; a connection that never declared `interactive_user` is an agent's. */
|
|
8
9
|
type InputClient = {
|
|
9
10
|
readonly interactiveUser: boolean;
|
|
11
|
+
readonly connectionOrigin?: ConnectionOrigin;
|
|
10
12
|
};
|
|
11
13
|
/** The same admission path serves classic stdio, shared sessions, and worker sessions. */
|
|
12
14
|
export declare function handleClientInput(session: AgentSession, command: RpcClientInput, io: InputOutput, client?: InputClient): Promise<void>;
|
|
@@ -72,6 +72,7 @@ export async function handleClientInput(session, command, io, client = { interac
|
|
|
72
72
|
...options,
|
|
73
73
|
source: "rpc",
|
|
74
74
|
interactiveUser: client.interactiveUser,
|
|
75
|
+
...(client.connectionOrigin === undefined ? {} : { connectionOrigin: client.connectionOrigin }),
|
|
75
76
|
...chain,
|
|
76
77
|
onQueuedInput: hooks.queued,
|
|
77
78
|
promptDisposition: (next) => {
|
|
@@ -136,6 +137,7 @@ export async function handleClientInput(session, command, io, client = { interac
|
|
|
136
137
|
expectedTurnId: command.expectedTurnId,
|
|
137
138
|
source: "rpc",
|
|
138
139
|
interactiveUser: client.interactiveUser,
|
|
140
|
+
...(client.connectionOrigin === undefined ? {} : { connectionOrigin: client.connectionOrigin }),
|
|
139
141
|
...chain,
|
|
140
142
|
onQueuedInput: hooks.queued,
|
|
141
143
|
};
|