@tea-agent/loop-agent 0.40.0-next.9 → 0.41.0-next.1
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/AGENTS.md +1 -1
- package/CHANGELOG.md +29 -20
- package/README.md +2 -2
- package/dist/application/dag/generate-task-dag.js +6 -2
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +9 -5
- package/dist/cli/program.js +38 -3
- package/dist/commands/client-recovery.js +8 -36
- package/dist/commands/init-model-catalog.js +22 -0
- package/dist/commands/task-advance.js +23 -1
- package/dist/commands/task-source-prepare.js +5 -0
- package/dist/commands/task-status.js +1 -0
- package/dist/executors/pi-sdk-executor.js +33 -5
- package/dist/executors/shell-executor.js +94 -20
- package/dist/governance/checks.js +1 -0
- package/dist/shared/operator/capabilities.js +9 -1
- package/dist/shared/pi-context-pressure/checkpoint.js +116 -0
- package/dist/shared/pi-context-pressure/compaction-policy.js +151 -0
- package/dist/shared/pi-context-pressure/env.js +58 -0
- package/dist/shared/pi-context-pressure/extension.js +100 -0
- package/dist/shared/pi-context-pressure/index.js +7 -0
- package/dist/shared/pi-context-pressure/overflow.js +252 -0
- package/dist/shared/pi-context-pressure/sift-bridge.js +386 -0
- package/dist/shared/pi-context-pressure/telemetry.js +51 -0
- package/dist/worker/console/chat/assistant-content.js +7 -0
- package/dist/worker/console/chat/chat-event-store.js +82 -11
- package/dist/worker/console/chat/goal-round-driver.js +72 -0
- package/dist/worker/console/chat/goals.js +300 -0
- package/dist/worker/console/chat/pi-runtime.js +286 -8
- package/dist/worker/console/chat/provider-error.js +2 -1
- package/dist/worker/console/chat/repo-browser.js +94 -12
- package/dist/worker/console/chat/routes.js +495 -30
- package/dist/worker/console/chat/semantic-activity.js +12 -11
- package/dist/worker/console/chat/session-catalog.js +48 -0
- package/dist/worker/console/chat/session-stats.js +96 -0
- package/dist/worker/console/chat/session-store.js +27 -18
- package/dist/worker/console/chat/shortcuts.js +7 -0
- package/dist/worker/console/chat/sift-bridge.js +1 -0
- package/dist/worker/console/chat/turn-process.js +60 -7
- package/dist/worker/console/dag-execution-receipt.js +20 -2
- package/dist/worker/console/operator-actions.js +20 -5
- package/dist/worker/console/operator-selection.js +6 -2
- package/dist/worker/console/pi-plugins.catalog.json +73658 -0
- package/dist/worker/console/pi-plugins.js +179 -0
- package/dist/worker/console/routes.js +246 -6
- package/dist/worker/console/server.js +173 -42
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-Bs-CDAXM.js → abnfDiagram-N423BO3Z-CV1qyRbn.js} +1 -1
- package/dist/worker/console/static/assets/{arc-CAkA3We3.js → arc-B_ljC4ma.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DEmI_zqr.js → architectureDiagram-T3A2C74G-DTBqsauL.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-uDHANebE.js → blockDiagram-VBNYF7ZC-CMZj6nYE.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-Bv97b4_B.js → c4Diagram-5PPSVZJV-C4ey_S7v.js} +1 -1
- package/dist/worker/console/static/assets/channel-BvYm_83n.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-shmQKsFz.js → chunk-2GRJ4B5K-CGcAlY6Z.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-BAraNvcN.js → chunk-2Q5K7J3B-XRSM64J1.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BirhxUop.js → chunk-5RXB4S5H-COSIFYbz.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-C9LgUJpR.js → chunk-5VM5RSS4-BwuaXiOK.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DrU-s-t7.js → chunk-6Q2QTUOP-DNbFuVlv.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-Tx-H_FA2.js → chunk-GF5L2VYU-C4DZKffE.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BjLtNFrR.js → chunk-JWPE2WC7-M3A6iczx.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-UNLVwSsy.js → chunk-KBJHAD2P-C5zDzQEM.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CBEUX8XD.js → chunk-RYQCIY6F-uAcUDub8.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-WTL7IZOq.js → chunk-XXDRQBXY-EPpz2uSP.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BNdy3f9F.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BNdy3f9F.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CkHG6W3s.js → cose-bilkent-JH36ORCC-BAkfFGTf.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BRzFqTaL.js → cynefin-VYW2F7L2-Cjb4d22A.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-2_CFp_i7.js → cynefinDiagram-MW4NZA55-Cf7QSgcu.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BJtsmdkE.js → dagre-VZM6K2ZE-UlyVBCtl.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-DNEFFo6w.js → diagram-7IWD3JNH-DWdRAZ8p.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B_IWV7eo.js → diagram-B4RE2ZJO-Y2ag2Pm0.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DvvRx2dz.js → diagram-LBJQPF4R-DBHcuPXI.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-CsibsWLg.js → diagram-Q27KOJAE-BK7eno2C.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-les_djBQ.js → diagram-UB23O5K3-Dbyku82r.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BEm2n7qJ.js → ebnfDiagram-BXEA7PRR--rS0Q41A.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BI7vHHVm.js → erDiagram-JOGREHBK-Cms8pJeK.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CUFCERrB.js → flowDiagram-UKHOOZJN-DouONwBy.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-wxyLiGWt.js → ganttDiagram-PKOTCBZU-DuKB5yDe.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Bt897K8Z.js → gitGraphDiagram-DS77QQ5N-DE6zp0SI.js} +1 -1
- package/dist/worker/console/static/assets/index-C39zQYik.js +451 -0
- package/dist/worker/console/static/assets/index-Crixt9tN.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DYNx_IPv.js → infoDiagram-6WML65LV-BLETv9SH.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-CzpLNEu6.js → ishikawaDiagram-WSZJBQD7-DyU-2lJb.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-97f9owNU.js → journeyDiagram-NVQOT4AX-BotFSygt.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BxM9hyBn.js → kanban-definition-27J2QSJJ-CqcAMgVk.js} +1 -1
- package/dist/worker/console/static/assets/{linear-ByuxcHvp.js → linear-Bf4U8h5G.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-DwPGvpCv.js → mermaid.core-D0JEVefG.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DCoqG9RT.js → mindmap-definition-FAOFIHXS-DEFgNgOo.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BLSBmBMx.js → pegDiagram-VL7TDLO6-CirKhxo1.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-B0t_0oJW.js → pieDiagram-7S7Q4E2Y-BlaQYInF.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-sTzDk7Nt.js → quadrantDiagram-CIZ2JOQS-BbyzqH4U.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-B3jRdATC.js → railroadDiagram-AXF67PYL-qA-XykV6.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D-cTtXEN.js → requirementDiagram-LRYGKXZP-Cc85Zcxc.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-B25igJRE.js → sankeyDiagram-W5VNT64P-CS7BVnoH.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-jW_lM1_D.js → sequenceDiagram-SI44F4Z6-DMQ-kKmZ.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-CrzTgsHE.js → sizeCapture-X5ZJPWSS-DXKs1dcE.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-B01MvlqE.js → stateDiagram-OKZ733FA-xG1ti87X.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-tYINJZ8x.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-YcF12FiU.js → swimlanes-SLNWSIFB-BQwmKGHd.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-OoBOXPiG.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-ClnRcg2s.js → timeline-definition-Z64GVDOM-BwL9oHtP.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-B6gr_z-q.js → vennDiagram-T6HMQDX7-BWk-i_Y1.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CvSgguGH.js → wardleyDiagram-T6FBY63Y-DAvETpEZ.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-23OtSZCb.js → xychartDiagram-ELKLHX3M-Cd4pASnF.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/useRunProgress.js +9 -2
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +95 -25
- package/dist/worker/console/static-src/operator-chat/compaction-message.js +35 -1
- package/dist/worker/console/static-src/operator-chat/dag-progress-link.js +22 -0
- package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +3 -1
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +26 -19
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +0 -11
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +14 -1
- package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +34 -4
- package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +28 -3
- package/dist/worker/console/static-src/operator-chat/workspace-reorder.js +18 -0
- package/dist/worker/console/static-src/pages/tasks/run-panel-progress.js +106 -0
- package/dist/worker/console/static-src/shell/useWorkspaces.js +150 -34
- package/dist/worker/console/static-src/shell/workspace-route.js +3 -1
- package/dist/worker/console/workspace-context.js +28 -3
- package/dist/worker/console/workspace-registry.js +333 -60
- package/dist/worker/observability/read-model.js +4 -0
- package/dist/worker/observe/static/inspect-bootstrap.d.ts +8 -0
- package/dist/worker/observe/static/inspect-bootstrap.js +57 -0
- package/dist/worker/observe/static/inspect-workspace.js +54 -10
- package/dist/worker/observe/static/operator-chrome.css +23 -4
- package/dist/worker/observe/static/operator-chrome.js +7 -2
- package/dist/worker/observe/static/styles.css +87 -25
- package/dist/worker/observe/static/views/dag-inspector.js +42 -1
- package/dist/worker/observe/static/views/dags.js +7 -5
- package/dist/worker/observe/static/views/session-timeline.js +135 -0
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +162 -8
- package/dist/workflows/dag/backend-test-pytest-collection.js +70 -2
- package/dist/workflows/dag/backend-test-result-contract.js +5 -1
- package/dist/workflows/dag/backend-test-scenario-param.js +339 -53
- package/dist/workflows/dag/backend-test-writer-completeness.js +11 -0
- package/dist/workflows/dag/init-hybrid.js +5 -4
- package/docs/architecture/runtime-boundaries.md +2 -1
- package/docs/templates/backend-test-dag.json +6 -5
- package/docs/templates/init-managed-agents.md +1 -1
- package/package.json +3 -2
- package/skills/loop-agent/references/command-reference.md +15 -1
- package/dist/worker/console/static/assets/channel-amxUpk7o.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-bejwBVIz.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-bejwBVIz.js +0 -1
- package/dist/worker/console/static/assets/index-BHhUOTri.css +0 -1
- package/dist/worker/console/static/assets/index-DqJbO3-p.js +0 -407
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BvFKEtm2.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DIB2eVg-.js +0 -8
|
@@ -36,10 +36,13 @@ import { OPERATOR_CHAT_ALLOWED_TOOLS, authorizeOperatorChatTool, assertNoWriteTo
|
|
|
36
36
|
import { createOperatorChatResourceLoader, } from "./resource-loader.js";
|
|
37
37
|
import { applySkillAutoInvocationPreferences, isAutoInvocationEnabled, loadResourcePreferences, skillResourceId, } from "./resource-preferences-store.js";
|
|
38
38
|
import { buildModelCallableToolSchemas } from "./tool-adapter.js";
|
|
39
|
+
import { applyProcessCompactionOverride, createCompactionController, createContextPressureExtension, createSiftBridgeState, housekeepSiftStash, isConsoleSiftEnabled, parseCompactionPolicy, parseSiftBridgeConfig, projectPromptPressure, deriveEffectiveWindow, projectSafeTelemetry, emptyContextPressureCounters, } from "../../../shared/pi-context-pressure/index.js";
|
|
40
|
+
import { incrementCounter as incrementPressureCounter } from "../../../shared/pi-context-pressure/telemetry.js";
|
|
39
41
|
import { resolveDefaultChatModel, resolveLowChatModel, } from "./model-resolver.js";
|
|
40
42
|
import { filterActiveInterviewTools } from "../interview/tools.js";
|
|
41
43
|
import { RUNTIME_CONTEXT_TEXT_MAX, redactRuntimeText, } from "./runtime-context.js";
|
|
42
44
|
import { buildTodoContinuationContext, } from "./todos.js";
|
|
45
|
+
import { buildGoalContinuationContext, } from "./goals.js";
|
|
43
46
|
/**
|
|
44
47
|
* Base system prompt fragment every General Operator Chat session receives.
|
|
45
48
|
*
|
|
@@ -64,10 +67,11 @@ export const OPERATOR_CHAT_SYSTEM_PROMPT_BASE = [
|
|
|
64
67
|
"You are General Operator Chat.",
|
|
65
68
|
"Use operator tools first (dagReport, dagRerunPlan, dagRerun). Pi read/write/edit/bash/grep/find/ls are available; obey repository AGENTS.md. apply_patch, full-tools, shell and coding-chat are denied. Prefer safe-read/safe-grep for probes.",
|
|
66
69
|
"No harness and asked to init: `loop-agent init instructions --repo-root .`, then `loop-agent init --repo-root . --profile full --merge`; verify with doctor, inspect and docs audit. Updates: `loop-agent init upgrade --repo-root . --json` until stable; `loop-agent init check-update` is read-only.",
|
|
67
|
-
"Long DAGs: prepareDagExecution → runDag → operationId. Never foreground-Bash task advance, tail/head pipes or nohup/Start-Process/start; do not end on accepted/queued/running or an operationId — NOT completion. Supervise operationGet/status/dagReport/dagDoctor/operationWait
|
|
68
|
-
"
|
|
69
|
-
"Iterate toward the goal in bounded rounds: first summarize the previous round's error in one line
|
|
70
|
+
"Long DAGs: prepareDagExecution → runDag → operationId. Never foreground-Bash task advance, tail/head pipes or nohup/Start-Process/start; do not end on accepted/queued/running or an operationId — NOT completion. Supervise operationGet/status/dagReport/dagDoctor/operationWait at 60s → 90s → 120s → 180s; reset to 60s on state change; 30-60s re-checks when stall suspected. operationWait MUST use afterSeq=previous nextSeq (recommendedNextCall). Heartbeats: not meaningful progress. Run id ≠ task id.",
|
|
71
|
+
"Failure: terminal runSummary first. With progress wait; else dagRerunPlan(fromNode=primaryFailure.nodeId); eligible → dagRerun: reruns the failed subgraph, keeps prior successes. Ineligible: Worker → workerTaskRetry; standalone → standaloneTaskRerun after contract adopt (regenerates the whole task, not node continue). Do not call dagResume; do not re-advance a run-failed task. Pre-run identical state twice: stop and report; never loop identical bash/read/grep. Approve only bounded writeSet inside allowedPaths, outside forbiddenPaths, with structured verification.",
|
|
72
|
+
"Iterate toward the goal in bounded rounds: first summarize the previous round's error in one line, fix its root cause — not the symptom — then rerun the failed node with a fresh plan. Never resubmit an unchanged fix; after two rounds failing the same way, stop and report evidence and remaining routes.",
|
|
70
73
|
"Continue until success, user stop, or no safe recovery remains; then stop and report failure/evidence. Never create a new task for a transient failure or replace repair-pi with direct edits.",
|
|
74
|
+
"Narrate long-DAG progress briefly at start, meaningful phase changes/findings, verification failure/recovery, prolonged quiet and terminal state: say what finished and what is next. Never narrate unchanged operationWait polls or heartbeats, or claim beyond structured facts.",
|
|
71
75
|
].join("\n");
|
|
72
76
|
/** Compose the inspectable system prompt actually injected into Operator Chat. */
|
|
73
77
|
export function composeOperatorChatSystemPrompt(input) {
|
|
@@ -257,6 +261,43 @@ export function createTodoCompactionContextExtension(options) {
|
|
|
257
261
|
},
|
|
258
262
|
};
|
|
259
263
|
}
|
|
264
|
+
/** Goal survives compaction and Turn boundaries; unlike Todo it is not reset
|
|
265
|
+
* at turn_start. The extension only injects after a successful compaction so
|
|
266
|
+
* ordinary contexts still rely on the model-callable get_goal source. */
|
|
267
|
+
export function createGoalCompactionContextExtension(options) {
|
|
268
|
+
return {
|
|
269
|
+
name: "operator-chat-goal-compaction-context",
|
|
270
|
+
hidden: true,
|
|
271
|
+
factory: (api) => {
|
|
272
|
+
let compacted = false;
|
|
273
|
+
api.on("session_compact", () => {
|
|
274
|
+
compacted = true;
|
|
275
|
+
});
|
|
276
|
+
api.on("context", (rawEvent) => {
|
|
277
|
+
if (!compacted)
|
|
278
|
+
return undefined;
|
|
279
|
+
const goal = options.registry.get(options.sessionId);
|
|
280
|
+
const context = buildGoalContinuationContext(goal);
|
|
281
|
+
if (!context)
|
|
282
|
+
return undefined;
|
|
283
|
+
const messages = Array.isArray(rawEvent.messages) ? rawEvent.messages : [];
|
|
284
|
+
return {
|
|
285
|
+
messages: [
|
|
286
|
+
...messages,
|
|
287
|
+
{
|
|
288
|
+
role: "custom",
|
|
289
|
+
customType: "operator-chat-goal-context",
|
|
290
|
+
content: context,
|
|
291
|
+
display: false,
|
|
292
|
+
details: { goal },
|
|
293
|
+
timestamp: Date.now(),
|
|
294
|
+
},
|
|
295
|
+
],
|
|
296
|
+
};
|
|
297
|
+
});
|
|
298
|
+
},
|
|
299
|
+
};
|
|
300
|
+
}
|
|
260
301
|
/**
|
|
261
302
|
* Build Pi custom ToolDefinition objects for every whitelisted operator action.
|
|
262
303
|
* Each tool's execute() routes into the operator action dispatcher (Gate 3).
|
|
@@ -406,6 +447,80 @@ async function buildTodoWriteTool(options) {
|
|
|
406
447
|
},
|
|
407
448
|
});
|
|
408
449
|
}
|
|
450
|
+
const GET_GOAL_DESCRIPTION = "Read the current session's durable cross-turn Goal, including revision, status, round limit, and whether this Console process is authorized to continue it.";
|
|
451
|
+
const CREATE_GOAL_DESCRIPTION = "Create a durable cross-turn Goal only when the user explicitly asks to start or create a Goal. Never silently upgrade an ordinary request. A session may have only one unfinished Goal.";
|
|
452
|
+
const UPDATE_GOAL_DESCRIPTION = "Finish or block the current durable Goal using its exact goalId and revision. Set complete only when the objective is genuinely achieved and no required work remains. Set blocked only when safe progress cannot continue; include the blocker.";
|
|
453
|
+
async function buildGoalTools(options) {
|
|
454
|
+
const [{ Type }, { defineTool }] = await Promise.all([
|
|
455
|
+
import("typebox"),
|
|
456
|
+
import("@earendil-works/pi-coding-agent"),
|
|
457
|
+
]);
|
|
458
|
+
const textResult = (value) => ({
|
|
459
|
+
content: [{ type: "text", text: JSON.stringify(value) }],
|
|
460
|
+
details: value,
|
|
461
|
+
});
|
|
462
|
+
return [
|
|
463
|
+
defineTool({
|
|
464
|
+
name: "get_goal",
|
|
465
|
+
label: "Get goal",
|
|
466
|
+
description: GET_GOAL_DESCRIPTION,
|
|
467
|
+
promptSnippet: GET_GOAL_DESCRIPTION,
|
|
468
|
+
parameters: Type.Object({}, { additionalProperties: false }),
|
|
469
|
+
async execute() {
|
|
470
|
+
return textResult({ goal: options.registry.get(options.sessionId) });
|
|
471
|
+
},
|
|
472
|
+
}),
|
|
473
|
+
defineTool({
|
|
474
|
+
name: "create_goal",
|
|
475
|
+
label: "Create goal",
|
|
476
|
+
description: CREATE_GOAL_DESCRIPTION,
|
|
477
|
+
promptSnippet: CREATE_GOAL_DESCRIPTION,
|
|
478
|
+
parameters: Type.Object({
|
|
479
|
+
objective: Type.String({
|
|
480
|
+
description: "Concrete final objective and completion standard explicitly requested by the user.",
|
|
481
|
+
}),
|
|
482
|
+
}, { additionalProperties: false }),
|
|
483
|
+
async execute(_toolCallId, params) {
|
|
484
|
+
if (!options.registry.consumeModelCreateGrant(options.sessionId)) {
|
|
485
|
+
return textResult({
|
|
486
|
+
ok: false,
|
|
487
|
+
code: "GOAL_EXPLICIT_REQUEST_REQUIRED",
|
|
488
|
+
message: "create_goal requires an explicit user request to create or start a Goal",
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
return textResult(options.registry.create({
|
|
492
|
+
sessionId: options.sessionId,
|
|
493
|
+
objective: params.objective,
|
|
494
|
+
}));
|
|
495
|
+
},
|
|
496
|
+
}),
|
|
497
|
+
defineTool({
|
|
498
|
+
name: "update_goal",
|
|
499
|
+
label: "Update goal",
|
|
500
|
+
description: UPDATE_GOAL_DESCRIPTION,
|
|
501
|
+
promptSnippet: UPDATE_GOAL_DESCRIPTION,
|
|
502
|
+
parameters: Type.Object({
|
|
503
|
+
goalId: Type.String(),
|
|
504
|
+
revision: Type.Number({ minimum: 1 }),
|
|
505
|
+
status: Type.Union([
|
|
506
|
+
Type.Literal("complete"),
|
|
507
|
+
Type.Literal("blocked"),
|
|
508
|
+
]),
|
|
509
|
+
blocker: Type.Optional(Type.String()),
|
|
510
|
+
}, { additionalProperties: false }),
|
|
511
|
+
async execute(_toolCallId, params) {
|
|
512
|
+
return textResult(options.registry.update({
|
|
513
|
+
sessionId: options.sessionId,
|
|
514
|
+
goalId: params.goalId,
|
|
515
|
+
expectedRevision: params.revision,
|
|
516
|
+
phase: params.status,
|
|
517
|
+
blocker: params.blocker,
|
|
518
|
+
arm: false,
|
|
519
|
+
}));
|
|
520
|
+
},
|
|
521
|
+
}),
|
|
522
|
+
];
|
|
523
|
+
}
|
|
409
524
|
/**
|
|
410
525
|
* Build the safe explore custom tools (roadmap M0-A / G16): safe-read,
|
|
411
526
|
* safe-grep, gitStatus, gitDiff. These replace the bare SDK bash/read/grep so
|
|
@@ -845,6 +960,9 @@ export class ConsolePiRuntime {
|
|
|
845
960
|
sessionModels = new Map();
|
|
846
961
|
/** SessionIds disposed while materialization was still in flight. */
|
|
847
962
|
disposedSessions = new Set();
|
|
963
|
+
compactionControllers = new Map();
|
|
964
|
+
siftStates = new Map();
|
|
965
|
+
/** Session-less tool definition caches (no AgentSession state). */
|
|
848
966
|
/** Session-less tool definition caches (no AgentSession state). */
|
|
849
967
|
operatorCustomToolsPromise;
|
|
850
968
|
exploreCustomToolsPromise;
|
|
@@ -892,14 +1010,25 @@ export class ConsolePiRuntime {
|
|
|
892
1010
|
registry: this.options.todoRegistry,
|
|
893
1011
|
})
|
|
894
1012
|
: undefined;
|
|
1013
|
+
const pressureExtension = sessionId
|
|
1014
|
+
? this.createContextPressureInlineExtension(sessionId)
|
|
1015
|
+
: undefined;
|
|
1016
|
+
const goalContextExtension = sessionId && this.options.goalRegistry
|
|
1017
|
+
? createGoalCompactionContextExtension({
|
|
1018
|
+
sessionId,
|
|
1019
|
+
registry: this.options.goalRegistry,
|
|
1020
|
+
})
|
|
1021
|
+
: undefined;
|
|
1022
|
+
const extraFactories = [
|
|
1023
|
+
todoContextExtension,
|
|
1024
|
+
goalContextExtension,
|
|
1025
|
+
pressureExtension,
|
|
1026
|
+
].filter((item) => Boolean(item));
|
|
895
1027
|
return {
|
|
896
1028
|
...base,
|
|
897
|
-
...(
|
|
1029
|
+
...(extraFactories.length > 0
|
|
898
1030
|
? {
|
|
899
|
-
extensionFactories: [
|
|
900
|
-
...inheritedFactories,
|
|
901
|
-
todoContextExtension,
|
|
902
|
-
],
|
|
1031
|
+
extensionFactories: [...inheritedFactories, ...extraFactories],
|
|
903
1032
|
}
|
|
904
1033
|
: {}),
|
|
905
1034
|
skillsOverride: (current) => {
|
|
@@ -924,6 +1053,126 @@ export class ConsolePiRuntime {
|
|
|
924
1053
|
this.cachedSkillPrefs = prefs;
|
|
925
1054
|
return prefs;
|
|
926
1055
|
}
|
|
1056
|
+
getCompactionController(sessionId) {
|
|
1057
|
+
let controller = this.compactionControllers.get(sessionId);
|
|
1058
|
+
if (!controller) {
|
|
1059
|
+
controller = createCompactionController();
|
|
1060
|
+
this.compactionControllers.set(sessionId, controller);
|
|
1061
|
+
}
|
|
1062
|
+
if (this.options.todoRegistry) {
|
|
1063
|
+
controller.todos = this.options.todoRegistry.get(sessionId);
|
|
1064
|
+
}
|
|
1065
|
+
return controller;
|
|
1066
|
+
}
|
|
1067
|
+
createContextPressureInlineExtension(sessionId) {
|
|
1068
|
+
const policy = parseCompactionPolicy();
|
|
1069
|
+
const siftEnabled = isConsoleSiftEnabled();
|
|
1070
|
+
let sift;
|
|
1071
|
+
if (siftEnabled.enabled) {
|
|
1072
|
+
sift = this.siftStates.get(sessionId);
|
|
1073
|
+
if (!sift) {
|
|
1074
|
+
const config = parseSiftBridgeConfig({
|
|
1075
|
+
sessionId,
|
|
1076
|
+
defaultStashRoot: path.join(this.options.sessionDir, "sift-stash"),
|
|
1077
|
+
});
|
|
1078
|
+
sift = createSiftBridgeState(config);
|
|
1079
|
+
sift.counters = this.getCompactionController(sessionId).counters;
|
|
1080
|
+
this.siftStates.set(sessionId, sift);
|
|
1081
|
+
const housekeep = housekeepSiftStash({
|
|
1082
|
+
root: path.dirname(config.stashDir),
|
|
1083
|
+
activeSessionId: sessionId,
|
|
1084
|
+
retentionDays: config.retentionDays,
|
|
1085
|
+
});
|
|
1086
|
+
if (housekeep.failed)
|
|
1087
|
+
sift.diagnostics.add("housekeep-failed");
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
return createContextPressureExtension({
|
|
1091
|
+
name: "operator-chat-context-pressure",
|
|
1092
|
+
policy,
|
|
1093
|
+
controller: this.getCompactionController(sessionId),
|
|
1094
|
+
sift,
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
applySessionCompactionOverride(sessionId, services, session) {
|
|
1098
|
+
const manager = services?.settingsManager;
|
|
1099
|
+
const window = session?.getSessionStats?.()?.contextUsage?.contextWindow;
|
|
1100
|
+
applyProcessCompactionOverride(manager, {
|
|
1101
|
+
policy: parseCompactionPolicy(),
|
|
1102
|
+
...(typeof window === "number" ? { contextWindow: window } : {}),
|
|
1103
|
+
});
|
|
1104
|
+
void sessionId;
|
|
1105
|
+
}
|
|
1106
|
+
async runPrePromptCompaction(sessionId, session, promptText, imageCount, onEvent) {
|
|
1107
|
+
const policy = parseCompactionPolicy();
|
|
1108
|
+
if (policy.mode === "off")
|
|
1109
|
+
return;
|
|
1110
|
+
const controller = this.getCompactionController(sessionId);
|
|
1111
|
+
const stats = session.getSessionStats?.();
|
|
1112
|
+
const window = stats?.contextUsage?.contextWindow;
|
|
1113
|
+
const tokens = stats?.contextUsage?.tokens;
|
|
1114
|
+
if (typeof window === "number" && window > 0) {
|
|
1115
|
+
this.applySessionCompactionOverride(sessionId, this.serviceScopes.get(sessionId) ?? {}, session);
|
|
1116
|
+
}
|
|
1117
|
+
const projection = projectPromptPressure({
|
|
1118
|
+
currentContextTokens: tokens,
|
|
1119
|
+
promptText,
|
|
1120
|
+
imageCount,
|
|
1121
|
+
});
|
|
1122
|
+
if (projection.usageUnavailable) {
|
|
1123
|
+
incrementPressureCounter(controller.counters, "usage-unavailable");
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
const effective = typeof window === "number" && window > 0
|
|
1127
|
+
? deriveEffectiveWindow(window, policy)
|
|
1128
|
+
: undefined;
|
|
1129
|
+
if (!effective || projection.projected < effective.triggerTokens)
|
|
1130
|
+
return;
|
|
1131
|
+
if (typeof session.compact !== "function")
|
|
1132
|
+
return;
|
|
1133
|
+
const compactionId = newCompactionId(sessionId);
|
|
1134
|
+
onEvent({
|
|
1135
|
+
type: "compaction_start",
|
|
1136
|
+
sessionId,
|
|
1137
|
+
compactionId,
|
|
1138
|
+
reason: "threshold",
|
|
1139
|
+
});
|
|
1140
|
+
controller.proactiveCompaction = true;
|
|
1141
|
+
try {
|
|
1142
|
+
incrementPressureCounter(controller.counters, "pre-prompt-threshold");
|
|
1143
|
+
const result = await session.compact();
|
|
1144
|
+
const snapshot = projectCompactSnapshot(result ?? {});
|
|
1145
|
+
onEvent({
|
|
1146
|
+
type: "compaction_end",
|
|
1147
|
+
sessionId,
|
|
1148
|
+
compactionId,
|
|
1149
|
+
reason: "threshold",
|
|
1150
|
+
status: "completed",
|
|
1151
|
+
willRetry: false,
|
|
1152
|
+
summary: snapshot.summary,
|
|
1153
|
+
...(snapshot.firstKeptEntryId
|
|
1154
|
+
? { firstKeptEntryId: snapshot.firstKeptEntryId }
|
|
1155
|
+
: {}),
|
|
1156
|
+
...(snapshot.tokensBefore === undefined
|
|
1157
|
+
? {}
|
|
1158
|
+
: { tokensBefore: snapshot.tokensBefore }),
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
catch {
|
|
1162
|
+
incrementPressureCounter(controller.counters, "l0-compact-failed");
|
|
1163
|
+
onEvent({
|
|
1164
|
+
type: "compaction_end",
|
|
1165
|
+
sessionId,
|
|
1166
|
+
compactionId,
|
|
1167
|
+
reason: "threshold",
|
|
1168
|
+
status: "failed",
|
|
1169
|
+
willRetry: false,
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
finally {
|
|
1173
|
+
controller.proactiveCompaction = false;
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
927
1176
|
/**
|
|
928
1177
|
* Resolve a skill in the current session inventory by server-issued resourceId.
|
|
929
1178
|
* Returns undefined when the id is unknown (caller maps to PI_SKILL_NOT_FOUND).
|
|
@@ -1156,6 +1405,7 @@ export class ConsolePiRuntime {
|
|
|
1156
1405
|
resourceLoader: services.resourceLoader,
|
|
1157
1406
|
settingsManager: services.settingsManager,
|
|
1158
1407
|
});
|
|
1408
|
+
this.applySessionCompactionOverride(sessionId, services, session);
|
|
1159
1409
|
this.revisions.set(sessionId, 1);
|
|
1160
1410
|
if (!this.thinkingSelections.has(sessionId)) {
|
|
1161
1411
|
this.thinkingSelections.set(sessionId, "auto");
|
|
@@ -1191,11 +1441,15 @@ export class ConsolePiRuntime {
|
|
|
1191
1441
|
const todoTool = this.options.todoRegistry
|
|
1192
1442
|
? await buildTodoWriteTool({ sessionId, registry: this.options.todoRegistry })
|
|
1193
1443
|
: undefined;
|
|
1444
|
+
const goalTools = this.options.goalRegistry
|
|
1445
|
+
? await buildGoalTools({ sessionId, registry: this.options.goalRegistry })
|
|
1446
|
+
: [];
|
|
1194
1447
|
return [
|
|
1195
1448
|
...operatorCustomTools,
|
|
1196
1449
|
...exploreCustomTools,
|
|
1197
1450
|
...(questionTool ? [questionTool] : []),
|
|
1198
1451
|
...(todoTool ? [todoTool] : []),
|
|
1452
|
+
...goalTools,
|
|
1199
1453
|
];
|
|
1200
1454
|
}
|
|
1201
1455
|
loadOperatorCustomTools() {
|
|
@@ -1470,6 +1724,9 @@ export class ConsolePiRuntime {
|
|
|
1470
1724
|
// the latest resource-preferences.json written by PATCH.
|
|
1471
1725
|
await this.refreshSkillPrefsCache().catch(() => undefined);
|
|
1472
1726
|
await session.reload();
|
|
1727
|
+
const services = this.serviceScopes.get(sessionId);
|
|
1728
|
+
if (services)
|
|
1729
|
+
this.applySessionCompactionOverride(sessionId, services, session);
|
|
1473
1730
|
}
|
|
1474
1731
|
catch (error) {
|
|
1475
1732
|
return {
|
|
@@ -1482,6 +1739,9 @@ export class ConsolePiRuntime {
|
|
|
1482
1739
|
this.reloadInFlight.delete(sessionId);
|
|
1483
1740
|
}
|
|
1484
1741
|
this.bumpRevision(sessionId);
|
|
1742
|
+
// Reload replaces the effective resource/session context. Persisted Goal
|
|
1743
|
+
// remains visible, but continuation authorization must be renewed by user.
|
|
1744
|
+
this.options.goalRegistry?.disarm(sessionId);
|
|
1485
1745
|
return { ok: true, revision: this.revisions.get(sessionId) ?? 0 };
|
|
1486
1746
|
}
|
|
1487
1747
|
/**
|
|
@@ -1927,6 +2187,9 @@ export class ConsolePiRuntime {
|
|
|
1927
2187
|
if (this.sessions.has(sessionId)) {
|
|
1928
2188
|
return { ok: true, handle: await this.snapshotActive(sessionId) };
|
|
1929
2189
|
}
|
|
2190
|
+
// A newly materialized Pi session must never inherit process-local Goal
|
|
2191
|
+
// activation from an earlier disposed runtime instance.
|
|
2192
|
+
this.options.goalRegistry?.disarm(sessionId);
|
|
1930
2193
|
if (!init.sessionFile) {
|
|
1931
2194
|
return {
|
|
1932
2195
|
ok: false,
|
|
@@ -2076,6 +2339,7 @@ export class ConsolePiRuntime {
|
|
|
2076
2339
|
});
|
|
2077
2340
|
await bindSessionExtensions(session);
|
|
2078
2341
|
pinOperatorChatTools(session);
|
|
2342
|
+
this.applySessionCompactionOverride(init.sessionId, services, session);
|
|
2079
2343
|
return {
|
|
2080
2344
|
session,
|
|
2081
2345
|
modelRuntime,
|
|
@@ -2117,6 +2381,12 @@ export class ConsolePiRuntime {
|
|
|
2117
2381
|
session?.hasPendingBashMessages === true,
|
|
2118
2382
|
};
|
|
2119
2383
|
}
|
|
2384
|
+
/** Low-cardinality context-pressure counters; never includes payload text. */
|
|
2385
|
+
getContextPressureTelemetry(sessionId) {
|
|
2386
|
+
const counters = this.compactionControllers.get(sessionId)?.counters ??
|
|
2387
|
+
this.siftStates.get(sessionId)?.counters;
|
|
2388
|
+
return projectSafeTelemetry(counters ?? emptyContextPressureCounters());
|
|
2389
|
+
}
|
|
2120
2390
|
/**
|
|
2121
2391
|
* Send a prompt and stream events. Gate 3: each tool invocation is
|
|
2122
2392
|
* re-authorized by authorizeOperatorChatTool before the dispatcher runs it
|
|
@@ -2159,6 +2429,9 @@ export class ConsolePiRuntime {
|
|
|
2159
2429
|
if (options?.turnId && this.options.todoRegistry) {
|
|
2160
2430
|
this.options.todoRegistry.startTurn(sessionId, options.turnId);
|
|
2161
2431
|
}
|
|
2432
|
+
if (this.options.goalRegistry) {
|
|
2433
|
+
this.options.goalRegistry.startTurn(sessionId, text);
|
|
2434
|
+
}
|
|
2162
2435
|
// AC-FIX-002: if Stop already fired before readiness, never start model work.
|
|
2163
2436
|
if (options?.signal?.aborted) {
|
|
2164
2437
|
return;
|
|
@@ -2212,6 +2485,7 @@ export class ConsolePiRuntime {
|
|
|
2212
2485
|
}
|
|
2213
2486
|
};
|
|
2214
2487
|
emitContextUsage();
|
|
2488
|
+
await this.runPrePromptCompaction(sessionId, session, text, options?.images?.length ?? 0, onEvent);
|
|
2215
2489
|
let stepStartedAt;
|
|
2216
2490
|
let firstTokenAt;
|
|
2217
2491
|
let pendingProviderError;
|
|
@@ -2461,13 +2735,17 @@ export class ConsolePiRuntime {
|
|
|
2461
2735
|
throw new Error("CHAT_TURN_ACTIVE");
|
|
2462
2736
|
if (typeof session.compact !== "function")
|
|
2463
2737
|
throw new Error("COMPACTION_UNSUPPORTED");
|
|
2738
|
+
incrementPressureCounter(this.getCompactionController(sessionId).counters, "manual");
|
|
2464
2739
|
const result = await session.compact(customInstructions);
|
|
2465
2740
|
return projectCompactSnapshot(result ?? {});
|
|
2466
2741
|
}
|
|
2467
2742
|
dispose(sessionId) {
|
|
2468
2743
|
// Mark disposed first so a late materialization never registers ready.
|
|
2469
2744
|
this.disposedSessions.add(sessionId);
|
|
2745
|
+
this.options.goalRegistry?.disarm(sessionId);
|
|
2470
2746
|
this.sessionInit.delete(sessionId);
|
|
2747
|
+
this.compactionControllers.delete(sessionId);
|
|
2748
|
+
this.siftStates.delete(sessionId);
|
|
2471
2749
|
const session = this.sessions.get(sessionId);
|
|
2472
2750
|
if (session) {
|
|
2473
2751
|
try {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isContextOverflow } from "../../../shared/pi-context-pressure/overflow.js";
|
|
1
2
|
const MAX_RAW_CHARS = 8_000;
|
|
2
3
|
const MAX_MESSAGE_CHARS = 1_000;
|
|
3
4
|
function asRecord(value) {
|
|
@@ -52,7 +53,7 @@ function classify(input) {
|
|
|
52
53
|
status === 403 ||
|
|
53
54
|
/认证|鉴权|unauthori[sz]ed|forbidden|authentication|invalid api[-_ ]?key/.test(haystack))
|
|
54
55
|
return "authentication";
|
|
55
|
-
if (
|
|
56
|
+
if (isContextOverflow(`${input.message}\n${input.raw}`))
|
|
56
57
|
return "context-overflow";
|
|
57
58
|
if (status === 429 ||
|
|
58
59
|
/rate.?limit|too many requests|请求.{0,8}(?:频繁|过多)|限流/.test(haystack))
|
|
@@ -4,8 +4,12 @@ import { lstat, open, readdir } from "node:fs/promises";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { guardReadPath, isSensitivePath, scrubSecrets, } from "./explore-tools.js";
|
|
6
6
|
const TREE_LIMIT = 500;
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
/** DSH-style bounded source read: large text files return this leading slice
|
|
8
|
+
* instead of being rejected solely because the complete file is larger. */
|
|
9
|
+
export const TEXT_PREVIEW_LIMIT = 512 * 1024;
|
|
10
|
+
/** Full-file ceiling for the sandboxed HTML preview route and its assets. */
|
|
11
|
+
export const HTML_PREVIEW_ASSET_LIMIT = 20 * 1024 * 1024;
|
|
12
|
+
export const REPO_HTML_PREVIEW_ROUTE_PREFIX = "/api/operator/v1/chat/repo/html/";
|
|
9
13
|
const SKIP_DIRS = new Set([
|
|
10
14
|
".git",
|
|
11
15
|
".harness",
|
|
@@ -32,6 +36,45 @@ export class RepoBrowserError extends Error {
|
|
|
32
36
|
function guardedCode(code) {
|
|
33
37
|
return code === "PATH_OUTSIDE_REPO" ? "OUT_OF_REPO" : code;
|
|
34
38
|
}
|
|
39
|
+
/** Match the path-encoded HTML preview route. Keeping workspace + file path in
|
|
40
|
+
* the URL path lets browser-relative CSS/image references retain their scope. */
|
|
41
|
+
export function matchRepoHtmlPreviewRoute(pathname) {
|
|
42
|
+
if (!pathname.startsWith(REPO_HTML_PREVIEW_ROUTE_PREFIX))
|
|
43
|
+
return null;
|
|
44
|
+
const rest = pathname.slice(REPO_HTML_PREVIEW_ROUTE_PREFIX.length);
|
|
45
|
+
const [workspaceId = "", ...pathSegments] = rest.split("/");
|
|
46
|
+
return { workspaceId, rawPath: pathSegments.join("/") };
|
|
47
|
+
}
|
|
48
|
+
export function repoHtmlAssetContentType(rawPath) {
|
|
49
|
+
const extension = path.extname(rawPath).toLowerCase();
|
|
50
|
+
const types = {
|
|
51
|
+
".html": "text/html; charset=utf-8",
|
|
52
|
+
".htm": "text/html; charset=utf-8",
|
|
53
|
+
".css": "text/css; charset=utf-8",
|
|
54
|
+
".js": "text/javascript; charset=utf-8",
|
|
55
|
+
".mjs": "text/javascript; charset=utf-8",
|
|
56
|
+
".json": "application/json; charset=utf-8",
|
|
57
|
+
".txt": "text/plain; charset=utf-8",
|
|
58
|
+
".svg": "image/svg+xml",
|
|
59
|
+
".png": "image/png",
|
|
60
|
+
".jpg": "image/jpeg",
|
|
61
|
+
".jpeg": "image/jpeg",
|
|
62
|
+
".gif": "image/gif",
|
|
63
|
+
".webp": "image/webp",
|
|
64
|
+
".avif": "image/avif",
|
|
65
|
+
".bmp": "image/bmp",
|
|
66
|
+
".ico": "image/x-icon",
|
|
67
|
+
".woff": "font/woff",
|
|
68
|
+
".woff2": "font/woff2",
|
|
69
|
+
".ttf": "font/ttf",
|
|
70
|
+
".otf": "font/otf",
|
|
71
|
+
".mp3": "audio/mpeg",
|
|
72
|
+
".wav": "audio/wav",
|
|
73
|
+
".mp4": "video/mp4",
|
|
74
|
+
".webm": "video/webm",
|
|
75
|
+
};
|
|
76
|
+
return types[extension] ?? "application/octet-stream";
|
|
77
|
+
}
|
|
35
78
|
export async function listRepoDirectory(repoRoot, rawPath) {
|
|
36
79
|
const root = rawPath === "";
|
|
37
80
|
const guard = await guardReadPath(repoRoot, root ? "." : rawPath);
|
|
@@ -92,8 +135,6 @@ export async function readRepoPreview(repoRoot, rawPath) {
|
|
|
92
135
|
const info = await handle.stat();
|
|
93
136
|
if (!info.isFile())
|
|
94
137
|
throw new RepoBrowserError(400, "NOT_A_FILE", "path is not a regular file");
|
|
95
|
-
if (info.size > READ_LIMIT)
|
|
96
|
-
throw new RepoBrowserError(413, "TOO_LARGE", `file exceeds ${READ_LIMIT} byte preview limit`);
|
|
97
138
|
const rechecked = await guardReadPath(repoRoot, rawPath);
|
|
98
139
|
if (!rechecked.ok)
|
|
99
140
|
throw new RepoBrowserError(403, guardedCode(rechecked.code), "file path changed while it was being opened");
|
|
@@ -103,9 +144,10 @@ export async function readRepoPreview(repoRoot, rawPath) {
|
|
|
103
144
|
current.ino !== info.ino)
|
|
104
145
|
throw new RepoBrowserError(403, "OUT_OF_REPO", "file identity changed while it was being opened");
|
|
105
146
|
size = info.size;
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
147
|
+
const readSize = Math.min(size, TEXT_PREVIEW_LIMIT);
|
|
148
|
+
bytes = Buffer.alloc(readSize);
|
|
149
|
+
const bytesRead = readSize
|
|
150
|
+
? (await handle.read(bytes, 0, readSize, 0)).bytesRead
|
|
109
151
|
: 0;
|
|
110
152
|
bytes = bytes.subarray(0, bytesRead);
|
|
111
153
|
}
|
|
@@ -114,17 +156,14 @@ export async function readRepoPreview(repoRoot, rawPath) {
|
|
|
114
156
|
}
|
|
115
157
|
if (bytes.includes(0))
|
|
116
158
|
throw new RepoBrowserError(415, "BINARY_UNSUPPORTED", "binary files cannot be previewed");
|
|
117
|
-
|
|
118
|
-
while (end > 0 && end < bytes.length && (bytes[end] & 0xc0) === 0x80)
|
|
119
|
-
end -= 1;
|
|
120
|
-
const content = scrubSecrets(bytes.subarray(0, end).toString("utf8")).scrubbed;
|
|
159
|
+
const content = scrubSecrets(bytes.toString("utf8")).scrubbed;
|
|
121
160
|
return {
|
|
122
161
|
path: guard.repoRelative,
|
|
123
162
|
kind: "text",
|
|
124
163
|
content,
|
|
125
164
|
language: path.extname(guard.repoRelative).slice(1).toLowerCase() || "text",
|
|
126
165
|
bytes: size,
|
|
127
|
-
truncated: bytes.length
|
|
166
|
+
truncated: size > bytes.length,
|
|
128
167
|
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
129
168
|
};
|
|
130
169
|
}
|
|
@@ -138,3 +177,46 @@ export async function readRepoPreview(repoRoot, rawPath) {
|
|
|
138
177
|
throw error;
|
|
139
178
|
}
|
|
140
179
|
}
|
|
180
|
+
/** Read one complete file for the sandboxed HTML iframe. Unlike source reads,
|
|
181
|
+
* this route needs complete HTML/CSS/image bytes so documents and relative
|
|
182
|
+
* assets render correctly; the separate 20 MiB cap keeps memory bounded. */
|
|
183
|
+
export async function readRepoHtmlAsset(repoRoot, rawPath) {
|
|
184
|
+
const guard = await guardReadPath(repoRoot, rawPath);
|
|
185
|
+
if (!guard.ok)
|
|
186
|
+
throw new RepoBrowserError(403, guardedCode(guard.code), guard.message);
|
|
187
|
+
try {
|
|
188
|
+
const handle = await open(guard.abs, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
189
|
+
try {
|
|
190
|
+
const info = await handle.stat();
|
|
191
|
+
if (!info.isFile())
|
|
192
|
+
throw new RepoBrowserError(400, "NOT_A_FILE", "path is not a regular file");
|
|
193
|
+
if (info.size > HTML_PREVIEW_ASSET_LIMIT)
|
|
194
|
+
throw new RepoBrowserError(413, "TOO_LARGE", `file exceeds ${HTML_PREVIEW_ASSET_LIMIT} byte HTML preview limit`);
|
|
195
|
+
const rechecked = await guardReadPath(repoRoot, rawPath);
|
|
196
|
+
if (!rechecked.ok)
|
|
197
|
+
throw new RepoBrowserError(403, guardedCode(rechecked.code), "file path changed while it was being opened");
|
|
198
|
+
const current = await lstat(rechecked.abs);
|
|
199
|
+
if (current.isSymbolicLink() ||
|
|
200
|
+
current.dev !== info.dev ||
|
|
201
|
+
current.ino !== info.ino)
|
|
202
|
+
throw new RepoBrowserError(403, "OUT_OF_REPO", "file identity changed while it was being opened");
|
|
203
|
+
return {
|
|
204
|
+
body: await handle.readFile(),
|
|
205
|
+
contentType: repoHtmlAssetContentType(guard.repoRelative),
|
|
206
|
+
bytes: info.size,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
finally {
|
|
210
|
+
await handle.close();
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
if (error instanceof RepoBrowserError)
|
|
215
|
+
throw error;
|
|
216
|
+
if (error.code === "ENOENT")
|
|
217
|
+
throw new RepoBrowserError(404, "NOT_FOUND", "file does not exist");
|
|
218
|
+
if (error.code === "ELOOP")
|
|
219
|
+
throw new RepoBrowserError(403, "OUT_OF_REPO", "symbolic links are not previewable");
|
|
220
|
+
throw error;
|
|
221
|
+
}
|
|
222
|
+
}
|