@hassangameryt/freeflow 0.7.2 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/NOTICE +14 -0
- package/README.md +151 -136
- package/capabilities/cognitive-routing/SKILL.md +110 -96
- package/capabilities/cognitive-routing/references/both-mode.md +36 -0
- package/capabilities/cognitive-routing/references/executor-mode.md +48 -0
- package/capabilities/cognitive-routing/references/helper-mode.md +47 -0
- package/capabilities/compaction/SKILL.md +49 -0
- package/capabilities/compaction/references/summary-format.md +239 -0
- package/command-surface.json +4 -0
- package/gemini-extension.json +1 -1
- package/hooks/shared/runtime-context.mjs +28 -30
- package/licenses/Apache-2.0.txt +55 -0
- package/package.json +22 -9
- package/pi-extension/README.md +41 -0
- package/pi-extension/dist/cognitive-routing/assembler.js +393 -0
- package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/config.js +40 -21
- package/pi-extension/dist/cognitive-routing/economics.js +45 -0
- package/pi-extension/dist/cognitive-routing/event-schema.js +388 -0
- package/pi-extension/dist/cognitive-routing/event-store.js +280 -0
- package/pi-extension/dist/cognitive-routing/gate.js +241 -0
- package/pi-extension/dist/cognitive-routing/handlers.js +899 -0
- package/pi-extension/dist/cognitive-routing/handoffs.js +350 -0
- package/pi-extension/dist/cognitive-routing/model-control.js +467 -0
- package/pi-extension/dist/cognitive-routing/projection.js +503 -0
- package/pi-extension/dist/cognitive-routing/provenance.js +81 -0
- package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/render.js +31 -10
- package/pi-extension/dist/cognitive-routing/runtime.js +396 -0
- package/pi-extension/dist/cognitive-routing/schemas.js +209 -0
- package/pi-extension/dist/cognitive-routing/session.js +221 -0
- package/pi-extension/dist/cognitive-routing/sources.js +291 -0
- package/pi-extension/dist/cognitive-routing/state.js +487 -0
- package/pi-extension/dist/cognitive-routing/status.js +80 -0
- package/pi-extension/dist/cognitive-routing/tools.js +60 -0
- package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/types.js +10 -12
- package/pi-extension/dist/compaction/carry.js +191 -0
- package/pi-extension/dist/compaction/config.js +22 -0
- package/pi-extension/dist/compaction/controller.js +471 -0
- package/pi-extension/dist/compaction/harness.js +77 -0
- package/pi-extension/dist/compaction/results.js +158 -0
- package/pi-extension/dist/compaction/thresholds.js +35 -0
- package/pi-extension/dist/compaction/tool.js +57 -0
- package/pi-extension/dist/host/branch.js +13 -0
- package/pi-extension/dist/host/canonical.js +12 -0
- package/pi-extension/dist/host/catalog.js +77 -0
- package/pi-extension/dist/host/config.js +374 -0
- package/pi-extension/dist/host/projection-tags.js +60 -0
- package/pi-extension/dist/host/prompts.js +82 -0
- package/pi-extension/dist/{session-sources → host}/read-only-session.js +112 -1
- package/pi-extension/dist/host/request-history.js +298 -0
- package/pi-extension/dist/host/runtime-state.js +189 -0
- package/pi-extension/dist/host/settings/freeflow-command.js +1570 -0
- package/pi-extension/dist/{settings/settings-tui.js → host/settings/settings-view.js} +186 -73
- package/pi-extension/dist/host/staging.js +28 -0
- package/pi-extension/dist/host/status.js +101 -0
- package/pi-extension/dist/index.js +441 -170
- package/pi-extension/dist/provider-support/cache/anchor.js +21 -0
- package/pi-extension/dist/provider-support/cache/anthropic.js +102 -0
- package/pi-extension/dist/provider-support/cache/health.js +73 -0
- package/pi-extension/dist/provider-support/cache/index.js +58 -0
- package/pi-extension/dist/provider-support/cache/keepalive.js +210 -0
- package/pi-extension/dist/provider-support/cache/monitor.js +17 -0
- package/pi-extension/dist/provider-support/cache/openai-responses.js +48 -0
- package/pi-extension/dist/provider-support/effort.js +13 -0
- package/pi-extension/dist/provider-support/index.js +45 -0
- package/pi-extension/dist/provider-support/openai/adapter.js +127 -0
- package/pi-extension/dist/provider-support/openai/history.js +127 -0
- package/pi-extension/dist/provider-support/openai/index.js +15 -0
- package/pi-extension/dist/provider-support/openai/session-state.js +105 -0
- package/pi-extension/dist/provider-support/routes.js +17 -0
- package/pi-extension/dist/tool-execution/apply-patch/grammar.js +23 -0
- package/pi-extension/dist/tool-execution/apply-patch/match.js +38 -0
- package/pi-extension/dist/tool-execution/apply-patch/parser.js +177 -0
- package/pi-extension/dist/tool-execution/apply-patch/plan.js +201 -0
- package/pi-extension/dist/tool-execution/apply-patch/tool.js +84 -0
- package/pi-extension/dist/tool-execution/background.js +238 -0
- package/pi-extension/dist/tool-execution/bash-guard.js +23 -0
- package/pi-extension/dist/tool-execution/config.js +22 -0
- package/pi-extension/dist/tool-execution/edit-messages.js +120 -0
- package/pi-extension/dist/tool-execution/file-state.js +138 -0
- package/pi-extension/dist/tool-execution/file-tracking.js +143 -0
- package/pi-extension/dist/tool-execution/prompt.js +54 -0
- package/pi-extension/dist/tool-execution/tools.js +16 -0
- package/pi-extension/freeflow/README.md +9 -0
- package/plugin.json +1 -1
- package/runtime/prompts/cognitive-routing.md +4 -4
- package/runtime/prompts/core.md +22 -75
- package/runtime/prompts/interaction-contract.md +15 -8
- package/runtime/prompts/tool-execution.md +38 -0
- package/runtime/prompts/working-method.md +108 -0
- package/skills/action-selection/SKILL.md +39 -47
- package/skills/action-selection/references/trajectory-stalls.md +2 -0
- package/skills/bypass/SKILL.md +2 -2
- package/skills/commit-work/SKILL.md +31 -83
- package/skills/commit-work/references/git-state-edges.md +1 -1
- package/skills/commit-work/references/simple-push.md +2 -0
- package/skills/commit-work/references/staging-decisions.md +1 -1
- package/skills/decision-gate/SKILL.md +31 -50
- package/skills/design-for-depth/SKILL.md +44 -53
- package/skills/design-for-depth/references/design-pressure-signals.md +46 -144
- package/skills/design-for-depth/references/implementation-decisions.md +4 -4
- package/skills/design-for-depth/references/interface-design-loop.md +5 -36
- package/skills/design-for-depth/references/module-and-dependency-design.md +2 -32
- package/skills/design-for-depth/references/state-and-failure-boundaries.md +2 -13
- package/skills/diagnose-failure/SKILL.md +39 -24
- package/skills/diagnose-failure/references/diagnostic-loop-catalog.md +2 -0
- package/skills/diagnose-failure/references/flaky-and-performance-diagnosis.md +2 -2
- package/skills/discuss/SKILL.md +37 -13
- package/skills/discuss/references/discussion-continuity.md +7 -27
- package/skills/evaluate-skill/SKILL.md +49 -80
- package/skills/evaluate-skill/references/evaluation-definition-schema.md +35 -5
- package/skills/evaluate-skill/references/evaluation-design.md +14 -14
- package/skills/evaluate-skill/references/execution-and-evidence.md +26 -5
- package/skills/evaluate-skill/references/review-and-revision.md +3 -1
- package/skills/evaluate-skill/scripts/lib/definitions.mjs +49 -8
- package/skills/evaluate-skill/scripts/lib/pi.mjs +14 -7
- package/skills/evaluate-skill/scripts/lib/review.mjs +196 -0
- package/skills/evaluate-skill/scripts/lib/runner.mjs +45 -8
- package/skills/evaluate-skill/scripts/lib/sandbox.mjs +2 -0
- package/skills/evaluate-skill/scripts/lib/trials.mjs +103 -0
- package/skills/evaluate-skill/scripts/lib/view.mjs +67 -1
- package/skills/evaluate-skill/scripts/pi-command-tool.mjs +144 -0
- package/skills/evaluate-skill/scripts/pi-guard.mjs +30 -13
- package/skills/evaluate-skill/scripts/skill-eval.mjs +114 -34
- package/skills/execute-work/SKILL.md +41 -9
- package/skills/execute-work/references/code-practices.md +2 -0
- package/skills/execute-work/references/execute-work-edges.md +11 -1
- package/skills/handoff/SKILL.md +1 -1
- package/skills/handoff/references/handoff-templates.md +1 -1
- package/skills/review-artifact/SKILL.md +42 -41
- package/skills/review-artifact/references/adjudicate-artifact-review.md +1 -1
- package/skills/review-artifact/references/independent-artifact-reviewer-contract.md +1 -1
- package/skills/review-work/SKILL.md +38 -32
- package/skills/review-work/references/adjudicate-work-review.md +1 -1
- package/skills/review-work/references/independent-work-reviewer-contract.md +1 -1
- package/skills/review-work/references/security-risk-lens.md +2 -4
- package/skills/setup-freeflow/SKILL.md +3 -1
- package/skills/setup-freeflow/references/activation-contract.md +7 -13
- package/skills/setup-freeflow/references/host-setup.md +3 -3
- package/skills/track-work/SKILL.md +113 -161
- package/skills/track-work/references/working-record-format.md +189 -350
- package/skills/track-work/scripts/lib/document.mjs +62 -6
- package/skills/track-work/scripts/lib/format.mjs +16 -1
- package/skills/track-work/scripts/lib/store.mjs +9 -8
- package/skills/track-work/scripts/lib/transitions.mjs +79 -4
- package/skills/track-work/scripts/working-record.mjs +34 -12
- package/skills/verify-work/SKILL.md +29 -20
- package/skills/verify-work/references/browser-runtime-evidence.md +1 -5
- package/skills/verify-work/references/integration-evidence.md +2 -0
- package/skills/verify-work/references/performance-evidence.md +3 -1
- package/skills/verify-work/references/test-design.md +3 -1
- package/skills/workflow/SKILL.md +65 -21
- package/skills/workflow/references/domain-skill-composition.md +5 -3
- package/skills/write-docs/SKILL.md +83 -0
- package/skills/write-docs/references/documentation-shapes.md +119 -0
- package/skills/write-docs/scripts/docs-check.mjs +139 -0
- package/skills/write-plan/SKILL.md +40 -40
- package/skills/write-plan/references/plan-shapes.md +1 -1
- package/skills/write-skill/SKILL.md +107 -147
- package/skills/write-skill/references/activation-boundaries.md +2 -0
- package/skills/write-skill/references/agent-first-instructions.md +22 -10
- package/skills/write-skill/references/progressive-disclosure.md +4 -2
- package/skills/write-skill/references/skill-dependency-graphs.md +2 -0
- package/skills/write-skill/scripts/lib/similarity.mjs +56 -0
- package/skills/write-skill/scripts/skill-author.mjs +18 -2
- package/skills/write-spec/SKILL.md +34 -41
- package/skills/write-spec/references/artifact-standards.md +1 -1
- package/skills/write-spec/references/decision-records.md +2 -0
- package/skills/write-spec/references/spec-shapes.md +1 -1
- package/capabilities/context-virtualization/SKILL.md +0 -103
- package/capabilities/conversation-history/SKILL.md +0 -53
- package/hooks/tests/check-host-adapters.test.mjs +0 -82
- package/hooks/tests/check-runtime-context-hook.sh +0 -212
- package/pi-extension/dist/cognitive-routing-v2/projection.js +0 -321
- package/pi-extension/dist/cognitive-routing-v2/runtime.js +0 -1585
- package/pi-extension/dist/cognitive-routing-v2/schemas.js +0 -107
- package/pi-extension/dist/cognitive-routing-v2/state.js +0 -612
- package/pi-extension/dist/cognitive-routing-v2/tools.js +0 -91
- package/pi-extension/dist/context-virtualization/commands.js +0 -89
- package/pi-extension/dist/context-virtualization/projector.js +0 -42
- package/pi-extension/dist/context-virtualization/runtime.js +0 -356
- package/pi-extension/dist/context-virtualization/state.js +0 -57
- package/pi-extension/dist/context-virtualization/types.js +0 -10
- package/pi-extension/dist/conversation-history/passages.js +0 -66
- package/pi-extension/dist/conversation-history/ranking.js +0 -105
- package/pi-extension/dist/conversation-history/runtime.js +0 -226
- package/pi-extension/dist/conversation-history/search.js +0 -56
- package/pi-extension/dist/conversation-history/types.js +0 -1
- package/pi-extension/dist/freeflow-context/resolver.js +0 -120
- package/pi-extension/dist/freeflow-context/runtime.js +0 -80
- package/pi-extension/dist/freeflow-context/source-projector.js +0 -95
- package/pi-extension/dist/freeflow-context/tool.js +0 -313
- package/pi-extension/dist/freeflow-context/types.js +0 -12
- package/pi-extension/dist/freeflow-context/visibility.js +0 -28
- package/pi-extension/dist/integrations/mcp-config.js +0 -29
- package/pi-extension/dist/runtime/runtime-context.js +0 -735
- package/pi-extension/dist/runtime/runtime-identity.js +0 -5
- package/pi-extension/dist/session-sources/events.js +0 -195
- package/pi-extension/dist/session-sources/sources.js +0 -233
- package/pi-extension/dist/settings/settings-ui.js +0 -1160
- package/pi-extension/dist/ui/text-component.js +0 -57
- package/pi-extension/src/cognitive-routing-v2/budget.ts +0 -42
- package/pi-extension/src/cognitive-routing-v2/config.ts +0 -143
- package/pi-extension/src/cognitive-routing-v2/projection.ts +0 -365
- package/pi-extension/src/cognitive-routing-v2/render.ts +0 -256
- package/pi-extension/src/cognitive-routing-v2/runtime.ts +0 -1634
- package/pi-extension/src/cognitive-routing-v2/schemas.ts +0 -109
- package/pi-extension/src/cognitive-routing-v2/state.ts +0 -622
- package/pi-extension/src/cognitive-routing-v2/tools.ts +0 -93
- package/pi-extension/src/cognitive-routing-v2/types.ts +0 -222
- package/pi-extension/src/context-virtualization/commands.ts +0 -111
- package/pi-extension/src/context-virtualization/projector.ts +0 -62
- package/pi-extension/src/context-virtualization/runtime.ts +0 -406
- package/pi-extension/src/context-virtualization/state.ts +0 -79
- package/pi-extension/src/context-virtualization/types.ts +0 -92
- package/pi-extension/src/conversation-history/passages.ts +0 -81
- package/pi-extension/src/conversation-history/ranking.ts +0 -155
- package/pi-extension/src/conversation-history/runtime.ts +0 -250
- package/pi-extension/src/conversation-history/search.ts +0 -79
- package/pi-extension/src/conversation-history/types.ts +0 -31
- package/pi-extension/src/freeflow-context/resolver.ts +0 -139
- package/pi-extension/src/freeflow-context/runtime.ts +0 -100
- package/pi-extension/src/freeflow-context/source-projector.ts +0 -106
- package/pi-extension/src/freeflow-context/tool.ts +0 -365
- package/pi-extension/src/freeflow-context/types.ts +0 -70
- package/pi-extension/src/freeflow-context/visibility.ts +0 -53
- package/pi-extension/src/index.ts +0 -368
- package/pi-extension/src/integrations/mcp-config.ts +0 -32
- package/pi-extension/src/runtime/runtime-context.ts +0 -838
- package/pi-extension/src/runtime/runtime-identity.ts +0 -8
- package/pi-extension/src/session-sources/events.ts +0 -208
- package/pi-extension/src/session-sources/read-only-session.ts +0 -170
- package/pi-extension/src/session-sources/sources.ts +0 -263
- package/pi-extension/src/settings/settings-tui.ts +0 -501
- package/pi-extension/src/settings/settings-ui.ts +0 -1422
- package/pi-extension/src/ui/text-component.ts +0 -66
- package/pi-extension/tests/cognitive-routing-v2/communication.test.js +0 -484
- package/pi-extension/tests/cognitive-routing-v2/compaction-input.test.js +0 -183
- package/pi-extension/tests/cognitive-routing-v2/native.test.js +0 -441
- package/pi-extension/tests/cognitive-routing-v2/performance.test.js +0 -87
- package/pi-extension/tests/cognitive-routing-v2/persistence.test.js +0 -245
- package/pi-extension/tests/cognitive-routing-v2/projection.test.js +0 -241
- package/pi-extension/tests/cognitive-routing-v2/recovery.test.js +0 -236
- package/pi-extension/tests/cognitive-routing-v2/render.test.js +0 -230
- package/pi-extension/tests/cognitive-routing-v2/runtime.test.js +0 -153
- package/pi-extension/tests/cognitive-routing-v2/stabilization.test.js +0 -521
- package/pi-extension/tests/cognitive-routing-v2/state.test.js +0 -275
- package/pi-extension/tests/context-virtualization/extension.test.js +0 -765
- package/pi-extension/tests/context-virtualization/projector.test.js +0 -110
- package/pi-extension/tests/context-virtualization/runtime.test.js +0 -216
- package/pi-extension/tests/conversation-history/runtime.test.js +0 -98
- package/pi-extension/tests/conversation-history/search.test.js +0 -97
- package/pi-extension/tests/fixtures/pi-host.js +0 -4
- package/pi-extension/tests/fixtures/routing-native.js +0 -188
- package/pi-extension/tests/integration/mode-free-core.test.js +0 -219
- package/pi-extension/tests/integration/pi-extension.test.js +0 -968
- package/pi-extension/tests/integration/prompt-architecture.test.js +0 -431
- package/pi-extension/tests/integration/unaffected-regressions.test.js +0 -478
- package/pi-extension/tests/settings/settings-tui.test.js +0 -243
- package/pi-extension/tsconfig.json +0 -16
- package/runtime/prompts/context-virtualization.md +0 -3
- package/runtime/prompts/conversation-history.md +0 -3
- package/skills/design-for-depth/references/software-design-philosophy.md +0 -202
- /package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/budget.js +0 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { assemble, EFFORTS } from "./history.js";
|
|
2
|
+
import { SessionState } from "./session-state.js";
|
|
3
|
+
import { isChatGPTSignIn } from "../routes.js";
|
|
4
|
+
// Only these qualified official routes receive effort-history adaptation; keys isolate models.
|
|
5
|
+
const SUPPORTED_MODELS = new Set(["gpt-6-astra", "gpt-6-luna", "gpt-6-sol"]);
|
|
6
|
+
/** The effort-history key for a qualified official GPT-6 route, or undefined when the route is not supported. */
|
|
7
|
+
export function effortHistoryRoute(model) {
|
|
8
|
+
if (!model || !SUPPORTED_MODELS.has(model.id)) return;
|
|
9
|
+
const codex = model.provider === "openai-codex" && model.api === "openai-codex-responses";
|
|
10
|
+
const api = model.provider === "openai" && model.api === "openai-responses";
|
|
11
|
+
if (!codex && !api) return;
|
|
12
|
+
if (model.baseUrl !== undefined && typeof model.baseUrl !== "string") return;
|
|
13
|
+
const base = (model.baseUrl ?? (codex ? "https://chatgpt.com/backend-api" : "https://api.openai.com/v1")).replace(
|
|
14
|
+
/\/+$/,
|
|
15
|
+
"",
|
|
16
|
+
);
|
|
17
|
+
const allowed = codex
|
|
18
|
+
? [
|
|
19
|
+
"https://chatgpt.com/backend-api",
|
|
20
|
+
"https://chatgpt.com/backend-api/codex",
|
|
21
|
+
"https://chatgpt.com/backend-api/codex/responses",
|
|
22
|
+
]
|
|
23
|
+
: ["https://api.openai.com/v1"];
|
|
24
|
+
if (!allowed.includes(base)) return;
|
|
25
|
+
return `${model.provider}/${model.api}/${model.id}`;
|
|
26
|
+
}
|
|
27
|
+
export function requestKey(payload, model) {
|
|
28
|
+
if (
|
|
29
|
+
!payload ||
|
|
30
|
+
!SUPPORTED_MODELS.has(payload.model) ||
|
|
31
|
+
model?.id !== payload.model ||
|
|
32
|
+
payload.store !== false ||
|
|
33
|
+
!Array.isArray(payload.input) ||
|
|
34
|
+
!EFFORTS.includes(payload.reasoning?.effort) ||
|
|
35
|
+
payload.previous_response_id ||
|
|
36
|
+
payload.conversation ||
|
|
37
|
+
payload.context_management ||
|
|
38
|
+
(payload.truncation && payload.truncation !== "disabled") ||
|
|
39
|
+
(payload.reasoning.mode && payload.reasoning.mode !== "standard") ||
|
|
40
|
+
payload.agents ||
|
|
41
|
+
payload.input.some((x) => x?.type === "configuration_update" || x?.type === "compaction_trigger")
|
|
42
|
+
)
|
|
43
|
+
return;
|
|
44
|
+
return effortHistoryRoute(model);
|
|
45
|
+
}
|
|
46
|
+
export class OpenAIEffortAdapter {
|
|
47
|
+
pi;
|
|
48
|
+
enabled;
|
|
49
|
+
monitor;
|
|
50
|
+
session;
|
|
51
|
+
generation = 0;
|
|
52
|
+
compacting = false;
|
|
53
|
+
queue = Promise.resolve();
|
|
54
|
+
statusSignature;
|
|
55
|
+
constructor(
|
|
56
|
+
pi,
|
|
57
|
+
// Global Freeflow disablement leaves provider requests and session history untouched.
|
|
58
|
+
enabled = () => true,
|
|
59
|
+
monitor,
|
|
60
|
+
) {
|
|
61
|
+
this.pi = pi;
|
|
62
|
+
this.enabled = enabled;
|
|
63
|
+
this.monitor = monitor;
|
|
64
|
+
}
|
|
65
|
+
reset(ctx) {
|
|
66
|
+
this.generation++;
|
|
67
|
+
this.session = undefined;
|
|
68
|
+
this.compacting = false;
|
|
69
|
+
this.status(ctx, undefined);
|
|
70
|
+
}
|
|
71
|
+
setCompacting(value) {
|
|
72
|
+
this.compacting = value;
|
|
73
|
+
}
|
|
74
|
+
status(_ctx, label) {
|
|
75
|
+
if (this.statusSignature === label) return;
|
|
76
|
+
this.statusSignature = label;
|
|
77
|
+
// A cache diagnostic, reported to /freeflow status rather than the settings footer.
|
|
78
|
+
this.monitor?.set("openai-effort", label);
|
|
79
|
+
}
|
|
80
|
+
async adapt(payload, ctx) {
|
|
81
|
+
const generation = this.generation;
|
|
82
|
+
const operation = this.queue.then(async () => {
|
|
83
|
+
// Sign in with ChatGPT rejects configuration_update items (live, 400), so effort history cannot apply.
|
|
84
|
+
const key = isChatGPTSignIn(ctx.model, ctx) ? undefined : requestKey(payload, ctx.model);
|
|
85
|
+
if (generation !== this.generation || this.compacting || !key || !this.enabled()) {
|
|
86
|
+
this.status(ctx, undefined);
|
|
87
|
+
return payload;
|
|
88
|
+
}
|
|
89
|
+
const label = ctx.model.id === "gpt-6-astra" ? "Astra" : ctx.model.id;
|
|
90
|
+
try {
|
|
91
|
+
const reader = ctx.sessionManager;
|
|
92
|
+
if (
|
|
93
|
+
typeof this.pi.appendEntry !== "function" ||
|
|
94
|
+
!reader?.getLeafId ||
|
|
95
|
+
!reader.getSessionId ||
|
|
96
|
+
!reader.getSessionFile
|
|
97
|
+
)
|
|
98
|
+
throw new Error("Native session persistence unavailable");
|
|
99
|
+
const id = reader.getSessionId(),
|
|
100
|
+
file = reader.getSessionFile();
|
|
101
|
+
if (!this.session || this.session.id !== id || this.session.file !== file)
|
|
102
|
+
this.session = { id, file, store: new SessionState(this.pi, reader) };
|
|
103
|
+
const store = this.session.store,
|
|
104
|
+
basis = reader.getLeafId();
|
|
105
|
+
const records = await store.records();
|
|
106
|
+
if (
|
|
107
|
+
generation !== this.generation ||
|
|
108
|
+
reader.getLeafId() !== basis ||
|
|
109
|
+
this.session?.store !== store ||
|
|
110
|
+
key !== requestKey(payload, ctx.model)
|
|
111
|
+
)
|
|
112
|
+
throw new Error("OpenAI effort request changed during preparation");
|
|
113
|
+
const result = assemble(payload, key, store.generation(), basis, records);
|
|
114
|
+
if (result.record) store.append(result.record);
|
|
115
|
+
this.status(ctx, `${label} ${result.effective} · cache baseline ${result.baseline}`);
|
|
116
|
+
return result.payload;
|
|
117
|
+
} catch {
|
|
118
|
+
// Pi swallows hook exceptions. Return the untouched full-history request,
|
|
119
|
+
// including its requested effort, instead of a partially pinned request.
|
|
120
|
+
this.status(ctx, `${label} cache adaptation unavailable · native effort`);
|
|
121
|
+
return payload;
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
this.queue = operation.catch(() => {});
|
|
125
|
+
return operation;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
// This namespace starts a fresh effort chain; old Astra entries are deliberately ignored.
|
|
3
|
+
export const ENTRY_TYPE = "freeflow-openai-effort-v1";
|
|
4
|
+
export const EFFORTS = ["low", "medium", "high", "xhigh", "max"];
|
|
5
|
+
export const hash = (value) => createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
6
|
+
const digest = (x) => typeof x === "string" && /^[a-f0-9]{64}$/.test(x);
|
|
7
|
+
const effort = (x) => EFFORTS.includes(x);
|
|
8
|
+
const identity = (x) => typeof x === "string" && x.length > 0 && x.length <= 1024;
|
|
9
|
+
export function parseAttempt(value) {
|
|
10
|
+
if (
|
|
11
|
+
!value ||
|
|
12
|
+
value.version !== 1 ||
|
|
13
|
+
!identity(value.id) ||
|
|
14
|
+
!(value.basis === null || identity(value.basis)) ||
|
|
15
|
+
!identity(value.key) ||
|
|
16
|
+
!identity(value.generation) ||
|
|
17
|
+
!digest(value.envelope) ||
|
|
18
|
+
!digest(value.inputHash) ||
|
|
19
|
+
!effort(value.baseline) ||
|
|
20
|
+
!effort(value.effective) ||
|
|
21
|
+
!(value.parent === null || identity(value.parent)) ||
|
|
22
|
+
!Number.isSafeInteger(value.length) ||
|
|
23
|
+
value.length < 0 ||
|
|
24
|
+
(value.update !== undefined &&
|
|
25
|
+
(!value.update ||
|
|
26
|
+
!Number.isSafeInteger(value.update.at) ||
|
|
27
|
+
value.update.at < 0 ||
|
|
28
|
+
value.update.at > value.length ||
|
|
29
|
+
!digest(value.update.prefixHash) ||
|
|
30
|
+
!effort(value.update.effort))) ||
|
|
31
|
+
Object.keys(value).some(
|
|
32
|
+
(k) =>
|
|
33
|
+
![
|
|
34
|
+
"version",
|
|
35
|
+
"id",
|
|
36
|
+
"basis",
|
|
37
|
+
"key",
|
|
38
|
+
"generation",
|
|
39
|
+
"envelope",
|
|
40
|
+
"baseline",
|
|
41
|
+
"effective",
|
|
42
|
+
"parent",
|
|
43
|
+
"length",
|
|
44
|
+
"inputHash",
|
|
45
|
+
"update",
|
|
46
|
+
].includes(k),
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
throw new Error("Invalid OpenAI effort history");
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
// Hash complete serialized items, including opaque reasoning; never persist their bodies.
|
|
53
|
+
export function prefixHashes(input) {
|
|
54
|
+
const result = [hash([])];
|
|
55
|
+
for (const item of input) result.push(hash([result.at(-1), item]));
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
export function envelopeHash(payload) {
|
|
59
|
+
const { input: _input, prompt_cache_key: _key, max_output_tokens: _max, stream: _stream, ...rest } = payload;
|
|
60
|
+
const { effort: _effort, ...reasoning } = rest.reasoning;
|
|
61
|
+
return hash({ ...rest, reasoning });
|
|
62
|
+
}
|
|
63
|
+
export function assemble(payload, key, generation, basis, records) {
|
|
64
|
+
const hashes = prefixHashes(payload.input),
|
|
65
|
+
envelope = envelopeHash(payload);
|
|
66
|
+
const applicable = records.filter((r) => r.key === key && r.generation === generation);
|
|
67
|
+
const compatible = applicable.filter((r) => r.envelope === envelope && hashes[r.length] === r.inputHash);
|
|
68
|
+
const prior = compatible.reduce((best, r) => (!best || r.length >= best.length ? r : best), undefined);
|
|
69
|
+
const baseline = prior?.baseline ?? applicable[0]?.baseline ?? payload.reasoning.effort;
|
|
70
|
+
const desired = payload.reasoning.effort;
|
|
71
|
+
const byId = new Map(records.map((r) => [r.id, r]));
|
|
72
|
+
const updates = [];
|
|
73
|
+
const seen = new Set();
|
|
74
|
+
let cursor = prior;
|
|
75
|
+
while (cursor) {
|
|
76
|
+
if (seen.has(cursor.id)) throw new Error("Cyclic OpenAI effort history");
|
|
77
|
+
seen.add(cursor.id);
|
|
78
|
+
if (cursor.update) {
|
|
79
|
+
if (hashes[cursor.update.at] !== cursor.update.prefixHash) throw new Error("OpenAI effort anchor changed");
|
|
80
|
+
updates.unshift(cursor.update);
|
|
81
|
+
}
|
|
82
|
+
if (cursor.parent === null) break;
|
|
83
|
+
const parent = byId.get(cursor.parent);
|
|
84
|
+
if (!parent || parent.key !== key || parent.generation !== generation || parent.baseline !== baseline)
|
|
85
|
+
throw new Error("OpenAI effort history parent unavailable");
|
|
86
|
+
cursor = parent;
|
|
87
|
+
}
|
|
88
|
+
let update;
|
|
89
|
+
if (desired !== (prior?.effective ?? baseline)) {
|
|
90
|
+
// Only insert before a new user item. Within a tool loop, append after all completed exchanges.
|
|
91
|
+
const last = payload.input.length - 1;
|
|
92
|
+
const at = payload.input[last]?.role === "user" && last >= (prior?.length ?? 0) ? last : payload.input.length;
|
|
93
|
+
update = { at, prefixHash: hashes[at], effort: desired };
|
|
94
|
+
if (updates.at(-1)?.at === at) throw new Error("Adjacent OpenAI effort updates");
|
|
95
|
+
updates.push(update);
|
|
96
|
+
}
|
|
97
|
+
if (updates.some((u, i) => i > 0 && u.at <= updates[i - 1].at)) throw new Error("Invalid OpenAI effort update order");
|
|
98
|
+
const output = structuredClone(payload);
|
|
99
|
+
output.input = [];
|
|
100
|
+
let nextUpdate = 0;
|
|
101
|
+
for (let i = 0; i <= payload.input.length; i++) {
|
|
102
|
+
if (updates[nextUpdate]?.at === i) {
|
|
103
|
+
output.input.push({ type: "configuration_update", reasoning: { effort: updates[nextUpdate++].effort } });
|
|
104
|
+
}
|
|
105
|
+
if (i < payload.input.length) output.input.push(structuredClone(payload.input[i]));
|
|
106
|
+
}
|
|
107
|
+
output.reasoning.effort = baseline;
|
|
108
|
+
// Records mark baselines and effort changes; a longer input under the same effort needs none.
|
|
109
|
+
const identical = !!prior && !update;
|
|
110
|
+
const record = identical
|
|
111
|
+
? undefined
|
|
112
|
+
: {
|
|
113
|
+
version: 1,
|
|
114
|
+
id: randomUUID(),
|
|
115
|
+
basis,
|
|
116
|
+
key,
|
|
117
|
+
generation,
|
|
118
|
+
envelope,
|
|
119
|
+
baseline,
|
|
120
|
+
effective: desired,
|
|
121
|
+
parent: prior?.id ?? null,
|
|
122
|
+
length: payload.input.length,
|
|
123
|
+
inputHash: hashes.at(-1),
|
|
124
|
+
...(update ? { update } : {}),
|
|
125
|
+
};
|
|
126
|
+
return { payload: output, record, baseline, effective: desired, rebased: !prior && applicable.length > 0 };
|
|
127
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { OpenAIEffortAdapter } from "./adapter.js";
|
|
2
|
+
import { trustStartedSession } from "../../host/read-only-session.js";
|
|
3
|
+
export function registerOpenAIEffortSupport(pi, enabled, monitor) {
|
|
4
|
+
const adapter = new OpenAIEffortAdapter(pi, enabled, monitor);
|
|
5
|
+
pi.on("session_start", (event, ctx) => {
|
|
6
|
+
trustStartedSession(event?.reason, ctx.sessionManager);
|
|
7
|
+
adapter.reset(ctx);
|
|
8
|
+
});
|
|
9
|
+
pi.on("session_shutdown", (_event, ctx) => adapter.reset(ctx));
|
|
10
|
+
pi.on("session_tree", (_event, ctx) => adapter.reset(ctx));
|
|
11
|
+
pi.on("session_before_compact", () => adapter.setCompacting(true));
|
|
12
|
+
pi.on("session_compact", (_event, ctx) => adapter.reset(ctx));
|
|
13
|
+
pi.on("session_compact_failed", () => adapter.setCompacting(false));
|
|
14
|
+
pi.on("before_provider_request", (event, ctx) => adapter.adapt(event.payload, ctx));
|
|
15
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { persistedBranchMatches } from "../../host/read-only-session.js";
|
|
2
|
+
import { ENTRY_TYPE, parseAttempt } from "./history.js";
|
|
3
|
+
import { cachedBranch } from "../../host/branch.js";
|
|
4
|
+
export class SessionState {
|
|
5
|
+
pi;
|
|
6
|
+
reader;
|
|
7
|
+
acknowledged = new Map();
|
|
8
|
+
fault;
|
|
9
|
+
constructor(pi, reader) {
|
|
10
|
+
this.pi = pi;
|
|
11
|
+
this.reader = reader;
|
|
12
|
+
}
|
|
13
|
+
branch() {
|
|
14
|
+
const entries = cachedBranch(this.reader);
|
|
15
|
+
let parent = null;
|
|
16
|
+
const seen = new Set();
|
|
17
|
+
for (const entry of entries) {
|
|
18
|
+
if (!entry.id || seen.has(entry.id) || entry.parentId !== parent)
|
|
19
|
+
throw new Error("Invalid OpenAI effort session ancestry");
|
|
20
|
+
seen.add(entry.id);
|
|
21
|
+
parent = entry.id;
|
|
22
|
+
}
|
|
23
|
+
if (parent !== this.reader.getLeafId()) throw new Error("OpenAI effort session leaf changed");
|
|
24
|
+
return entries;
|
|
25
|
+
}
|
|
26
|
+
async records() {
|
|
27
|
+
if (this.fault) throw new Error(this.fault);
|
|
28
|
+
const branch = this.branch();
|
|
29
|
+
// Model visits and branch summaries preserve compatible history; compaction does not. A branch summary
|
|
30
|
+
// follows its branch point, leaving the path before it unchanged. Records written while branch summaries
|
|
31
|
+
// still started a generation carry that earlier boundary and stay readable.
|
|
32
|
+
const boundaries = new Map(),
|
|
33
|
+
earlier = new Map();
|
|
34
|
+
let epoch = "root",
|
|
35
|
+
earlierEpoch = "root";
|
|
36
|
+
for (const entry of branch) {
|
|
37
|
+
if (entry.type === "compaction") epoch = entry.id;
|
|
38
|
+
if (entry.type === "compaction" || entry.type === "branch_summary") earlierEpoch = entry.id;
|
|
39
|
+
boundaries.set(entry.id, epoch);
|
|
40
|
+
earlier.set(entry.id, earlierEpoch);
|
|
41
|
+
}
|
|
42
|
+
const entries = branch.filter((e) => e.type === "custom" && e.customType === ENTRY_TYPE);
|
|
43
|
+
const records = entries.map((e) => {
|
|
44
|
+
const record = parseAttempt(e.data);
|
|
45
|
+
if (
|
|
46
|
+
record.basis !== e.parentId &&
|
|
47
|
+
!(this.reader.getHeader?.()?.parentSession && !branch.some((entry) => entry.id === record.basis))
|
|
48
|
+
)
|
|
49
|
+
throw new Error("OpenAI effort attempt anchor mismatch");
|
|
50
|
+
if (record.generation !== boundaries.get(e.id) && record.generation !== earlier.get(e.id))
|
|
51
|
+
throw new Error("OpenAI effort history generation unavailable");
|
|
52
|
+
return record;
|
|
53
|
+
});
|
|
54
|
+
if (new Set(records.map((r) => r.id)).size !== records.length) throw new Error("Duplicate OpenAI effort attempt");
|
|
55
|
+
const parents = new Map();
|
|
56
|
+
for (const record of records) {
|
|
57
|
+
const parent = record.parent === null ? undefined : parents.get(record.parent);
|
|
58
|
+
if (
|
|
59
|
+
record.parent !== null &&
|
|
60
|
+
(!parent ||
|
|
61
|
+
parent.key !== record.key ||
|
|
62
|
+
parent.generation !== record.generation ||
|
|
63
|
+
parent.envelope !== record.envelope ||
|
|
64
|
+
parent.baseline !== record.baseline ||
|
|
65
|
+
parent.length > record.length)
|
|
66
|
+
)
|
|
67
|
+
throw new Error("Invalid OpenAI effort history parent");
|
|
68
|
+
const expected = record.update?.effort ?? parent?.effective ?? record.baseline;
|
|
69
|
+
if (record.effective !== expected || (record.update && record.update.at < (parent?.length ?? 0)))
|
|
70
|
+
throw new Error("Invalid OpenAI effective effort");
|
|
71
|
+
parents.set(record.id, record);
|
|
72
|
+
}
|
|
73
|
+
const unknown = entries.some((e) => this.acknowledged.get(e.id) !== JSON.stringify(e));
|
|
74
|
+
const file = this.reader.getSessionFile();
|
|
75
|
+
if (unknown && file && branch.some((e) => e.type === "message" && e.message?.role === "assistant")) {
|
|
76
|
+
const leaf = this.reader.getLeafId();
|
|
77
|
+
if (!(await persistedBranchMatches(this.reader, leaf, branch)) || this.reader.getLeafId() !== leaf)
|
|
78
|
+
throw new Error("OpenAI effort persisted ancestry differs");
|
|
79
|
+
}
|
|
80
|
+
for (const entry of entries) this.acknowledged.set(entry.id, JSON.stringify(entry));
|
|
81
|
+
return records;
|
|
82
|
+
}
|
|
83
|
+
generation() {
|
|
84
|
+
let generation = "root";
|
|
85
|
+
for (const entry of this.branch()) {
|
|
86
|
+
if (entry.type === "compaction") generation = entry.id;
|
|
87
|
+
}
|
|
88
|
+
return generation;
|
|
89
|
+
}
|
|
90
|
+
append(record) {
|
|
91
|
+
try {
|
|
92
|
+
if (this.reader.getLeafId() !== record.basis) throw new Error("OpenAI effort request ancestry changed");
|
|
93
|
+
this.pi.appendEntry(ENTRY_TYPE, record);
|
|
94
|
+
const entry = this.branch().find(
|
|
95
|
+
(e) => e.type === "custom" && e.customType === ENTRY_TYPE && e.data?.id === record.id,
|
|
96
|
+
);
|
|
97
|
+
if (!entry || entry.parentId !== record.basis || JSON.stringify(entry.data) !== JSON.stringify(record))
|
|
98
|
+
throw new Error("OpenAI effort append not acknowledged");
|
|
99
|
+
this.acknowledged.set(entry.id, JSON.stringify(entry));
|
|
100
|
+
} catch (error) {
|
|
101
|
+
this.fault = "OpenAI effort append uncertain; native effort retained until session recovery";
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const OPENAI_API = "https://api.openai.com/v1";
|
|
2
|
+
/**
|
|
3
|
+
* OpenAI's Sign in with ChatGPT (Pi 0.99): the openai provider's Responses API at api.openai.com with a ChatGPT
|
|
4
|
+
* credential instead of an API key. It reaches the ChatGPT backend. Live on gpt-6-luna and gpt-6.1-sol (2026-09-30) it
|
|
5
|
+
* rejected prompt_cache_breakpoint, max_output_tokens and configuration_update items with HTTP 400, and reused the
|
|
6
|
+
* whole cached prefix after 25 appended message endings without breakpoints. Freeflow sends its requests as Pi
|
|
7
|
+
* built them.
|
|
8
|
+
*/
|
|
9
|
+
export function isChatGPTSignIn(model, ctx) {
|
|
10
|
+
if (model?.provider !== "openai" || model.api !== "openai-responses") return false;
|
|
11
|
+
if ((model.baseUrl ?? OPENAI_API).replace(/\/+$/, "") !== OPENAI_API) return false;
|
|
12
|
+
try {
|
|
13
|
+
return ctx?.modelRegistry?.isUsingOAuth?.(model) === true;
|
|
14
|
+
} catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// The apply_patch Lark grammar from OpenAI Codex (codex-rs/core/assets/tools/apply_patch.lark at bcd6d9ab),
|
|
2
|
+
// Copyright 2025 OpenAI, licensed under the Apache License 2.0 (https://www.apache.org/licenses/LICENSE-2.0).
|
|
3
|
+
// GPT models are trained on this exact grammar; keep it byte-identical to upstream.
|
|
4
|
+
export const APPLY_PATCH_LARK_GRAMMAR = `start: begin_patch hunk+ end_patch
|
|
5
|
+
begin_patch: "*** Begin Patch" LF
|
|
6
|
+
end_patch: "*** End Patch" LF?
|
|
7
|
+
|
|
8
|
+
hunk: add_hunk | delete_hunk | update_hunk
|
|
9
|
+
add_hunk: "*** Add File: " filename LF add_line+
|
|
10
|
+
delete_hunk: "*** Delete File: " filename LF
|
|
11
|
+
update_hunk: "*** Update File: " filename LF change_move? change?
|
|
12
|
+
|
|
13
|
+
filename: /(.+)/
|
|
14
|
+
add_line: "+" /(.*)/ LF -> line
|
|
15
|
+
|
|
16
|
+
change_move: "*** Move to: " filename LF
|
|
17
|
+
change: (change_context | change_line)+ eof_line?
|
|
18
|
+
change_context: ("@@" | "@@ " /(.+)/) LF
|
|
19
|
+
change_line: ("+" | "-" | " ") /(.*)/ LF
|
|
20
|
+
eof_line: "*** End of File" LF
|
|
21
|
+
|
|
22
|
+
%import common.LF
|
|
23
|
+
`;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Locating a hunk's lines in a file, with the four passes of OpenAI Codex's seek_sequence
|
|
2
|
+
// (codex-rs/apply-patch/src/seek_sequence.rs at bcd6d9ab, Apache-2.0): exact, then ignoring trailing whitespace,
|
|
3
|
+
// then ignoring surrounding whitespace, then after normalizing Unicode punctuation and spaces.
|
|
4
|
+
function normalise(line) {
|
|
5
|
+
return line
|
|
6
|
+
.trim()
|
|
7
|
+
.replace(/[‐‑‒–—―−]/g, "-")
|
|
8
|
+
.replace(/[‘’‚‛]/g, "'")
|
|
9
|
+
.replace(/[“”„‟]/g, '"')
|
|
10
|
+
.replace(/[ - ]/g, " ");
|
|
11
|
+
}
|
|
12
|
+
const PASSES = [(line) => line, (line) => line.trimEnd(), (line) => line.trim(), normalise];
|
|
13
|
+
function matchesAt(lines, pattern, at, pass) {
|
|
14
|
+
const compare = PASSES[pass];
|
|
15
|
+
for (let i = 0; i < pattern.length; i++) if (compare(lines[at + i]) !== compare(pattern[i])) return false;
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The first index at or after `start` where `pattern` matches, and the pass that matched. With `endOfFile`, the
|
|
20
|
+
* search starts where the pattern would end the file (falling back to `start` when that is earlier than the file).
|
|
21
|
+
*/
|
|
22
|
+
export function seekSequence(lines, pattern, start, endOfFile) {
|
|
23
|
+
if (pattern.length === 0) return { index: start, pass: 0 };
|
|
24
|
+
if (pattern.length > lines.length) return undefined;
|
|
25
|
+
const from = endOfFile ? Math.max(lines.length - pattern.length, start) : start;
|
|
26
|
+
for (let pass = 0; pass < PASSES.length; pass++)
|
|
27
|
+
for (let i = from; i <= lines.length - pattern.length; i++)
|
|
28
|
+
if (matchesAt(lines, pattern, i, pass)) return { index: i, pass };
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
/** Other places in the whole file where the pattern matches with the same pass: a visible sign of ambiguity. */
|
|
32
|
+
export function otherMatches(lines, pattern, index, pass) {
|
|
33
|
+
if (pattern.length === 0) return [];
|
|
34
|
+
const found = [];
|
|
35
|
+
for (let i = 0; i <= lines.length - pattern.length; i++)
|
|
36
|
+
if (i !== index && matchesAt(lines, pattern, i, pass)) found.push(i);
|
|
37
|
+
return found;
|
|
38
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// A port of OpenAI Codex's apply_patch parser (codex-rs/apply-patch/src/{parser,streaming_parser}.rs at bcd6d9ab,
|
|
2
|
+
// Apache-2.0), including its lenient forms, so patches GPT models write for Codex parse the same way here.
|
|
3
|
+
export class PatchParseError extends Error {
|
|
4
|
+
line;
|
|
5
|
+
constructor(message, line) {
|
|
6
|
+
super(line === undefined ? message : `${message} (patch line ${line})`);
|
|
7
|
+
this.line = line;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
const BEGIN = "*** Begin Patch";
|
|
11
|
+
const END = "*** End Patch";
|
|
12
|
+
const ADD = "*** Add File: ";
|
|
13
|
+
const DELETE = "*** Delete File: ";
|
|
14
|
+
const UPDATE = "*** Update File: ";
|
|
15
|
+
const MOVE = "*** Move to: ";
|
|
16
|
+
const EOF_MARKER = "*** End of File";
|
|
17
|
+
const ENVIRONMENT = "*** Environment ID: ";
|
|
18
|
+
const HEADERS = "Valid hunk headers: '*** Add File: {path}', '*** Delete File: {path}', '*** Update File: {path}'";
|
|
19
|
+
const UNEXPECTED = (line) =>
|
|
20
|
+
`Unexpected line found in update hunk: '${line}'. Every line should start with ' ' (context line), '+' (added line), or '-' (removed line)`;
|
|
21
|
+
function boundaries(lines) {
|
|
22
|
+
const strict = (candidate) => {
|
|
23
|
+
const first = candidate[0]?.trim(),
|
|
24
|
+
last = candidate.at(-1)?.trim();
|
|
25
|
+
if (first !== BEGIN) throw new PatchParseError(`The first line of the patch must be '${BEGIN}'`);
|
|
26
|
+
if (last !== END) throw new PatchParseError(`The last line of the patch must be '${END}'`);
|
|
27
|
+
return candidate;
|
|
28
|
+
};
|
|
29
|
+
try {
|
|
30
|
+
return strict(lines);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
// Codex's lenient mode: a heredoc wrapper, as GPT models write for a shell invocation. The optional
|
|
33
|
+
// `apply_patch` prefix covers the same wrapper copied from a shell command.
|
|
34
|
+
const first = lines[0]?.replace(/^apply_patch\s+/, "");
|
|
35
|
+
if (["<<EOF", "<<'EOF'", '<<"EOF"'].includes(first ?? "") && lines.at(-1)?.endsWith("EOF") && lines.length >= 4)
|
|
36
|
+
return strict(lines.slice(1, -1));
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export function parsePatch(text) {
|
|
41
|
+
const lines = boundaries(text.trim().split(/\r?\n/));
|
|
42
|
+
const hunks = [];
|
|
43
|
+
let mode = "started";
|
|
44
|
+
let updateHeaderLine = 0;
|
|
45
|
+
let environment = false;
|
|
46
|
+
const lastUpdate = () => {
|
|
47
|
+
const hunk = hunks.at(-1);
|
|
48
|
+
return hunk?.kind === "update" ? hunk : undefined;
|
|
49
|
+
};
|
|
50
|
+
const ensureUpdateNotEmpty = (line, number) => {
|
|
51
|
+
const update = mode === "update" ? lastUpdate() : undefined;
|
|
52
|
+
if (!update) return;
|
|
53
|
+
if (update.chunks.length === 0)
|
|
54
|
+
throw new PatchParseError(`Update file hunk for path '${update.path}' is empty`, updateHeaderLine);
|
|
55
|
+
const chunk = update.chunks.at(-1);
|
|
56
|
+
if (chunk.oldLines.length === 0 && chunk.newLines.length === 0)
|
|
57
|
+
throw new PatchParseError(line === END ? "Update hunk does not contain any lines" : UNEXPECTED(line), number);
|
|
58
|
+
};
|
|
59
|
+
const header = (trimmed, number) => {
|
|
60
|
+
if (mode === "started" && trimmed.startsWith(ENVIRONMENT) && hunks.length === 0) {
|
|
61
|
+
if (environment) throw new PatchParseError("apply_patch environment_id cannot be specified more than once");
|
|
62
|
+
if (!trimmed.slice(ENVIRONMENT.length).trim())
|
|
63
|
+
throw new PatchParseError("apply_patch environment_id cannot be empty");
|
|
64
|
+
environment = true;
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
for (const [marker, kind] of [
|
|
68
|
+
[END, "end"],
|
|
69
|
+
[ADD, "add"],
|
|
70
|
+
[DELETE, "delete"],
|
|
71
|
+
[UPDATE, "update"],
|
|
72
|
+
]) {
|
|
73
|
+
if (kind === "end" ? trimmed !== END : !trimmed.startsWith(marker)) continue;
|
|
74
|
+
ensureUpdateNotEmpty(trimmed, number);
|
|
75
|
+
const path = trimmed.slice(marker.length);
|
|
76
|
+
if (kind === "end") mode = "ended";
|
|
77
|
+
else if (kind === "add") {
|
|
78
|
+
hunks.push({ kind: "add", path, contents: "" });
|
|
79
|
+
mode = "add";
|
|
80
|
+
} else if (kind === "delete") {
|
|
81
|
+
hunks.push({ kind: "delete", path });
|
|
82
|
+
mode = "delete";
|
|
83
|
+
} else {
|
|
84
|
+
hunks.push({ kind: "update", path, chunks: [] });
|
|
85
|
+
mode = "update";
|
|
86
|
+
updateHeaderLine = number;
|
|
87
|
+
}
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
return false;
|
|
91
|
+
};
|
|
92
|
+
const chunk = (update) => {
|
|
93
|
+
if (update.chunks.length === 0) update.chunks.push({ oldLines: [], newLines: [], context: [], endOfFile: false });
|
|
94
|
+
return update.chunks.at(-1);
|
|
95
|
+
};
|
|
96
|
+
lines.slice(1).forEach((line, offset) => {
|
|
97
|
+
const number = offset + 2;
|
|
98
|
+
const trimmed = line.trim();
|
|
99
|
+
if (mode === "ended") {
|
|
100
|
+
if (trimmed) throw new PatchParseError(`The last line of the patch must be '${END}'`, number);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (mode === "started") {
|
|
104
|
+
if (!header(trimmed, number))
|
|
105
|
+
throw new PatchParseError(`'${trimmed}' is not a valid hunk header. ${HEADERS}`, number);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (mode === "add") {
|
|
109
|
+
if (header(trimmed, number)) return;
|
|
110
|
+
const add = hunks.at(-1);
|
|
111
|
+
if (line.startsWith("+") && add?.kind === "add") {
|
|
112
|
+
add.contents += `${line.slice(1)}\n`;
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
throw new PatchParseError(`'${trimmed}' is not a valid hunk header. ${HEADERS}`, number);
|
|
116
|
+
}
|
|
117
|
+
if (mode === "delete") {
|
|
118
|
+
if (header(trimmed, number)) return;
|
|
119
|
+
throw new PatchParseError(`'${trimmed}' is not a valid hunk header. ${HEADERS}`, number);
|
|
120
|
+
}
|
|
121
|
+
// Update file.
|
|
122
|
+
const updateLine = line.trimEnd();
|
|
123
|
+
if (header(updateLine, number)) return;
|
|
124
|
+
const update = lastUpdate();
|
|
125
|
+
const current = update.chunks.at(-1);
|
|
126
|
+
if (current?.endOfFile) {
|
|
127
|
+
if (!updateLine) return;
|
|
128
|
+
if (updateLine !== "@@" && !updateLine.startsWith("@@ "))
|
|
129
|
+
throw new PatchParseError(`Expected update hunk to start with a @@ context marker, got: '${line}'`, number);
|
|
130
|
+
}
|
|
131
|
+
if (update.chunks.length === 0 && update.moveTo === undefined && updateLine.startsWith(MOVE)) {
|
|
132
|
+
update.moveTo = updateLine.slice(MOVE.length);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const isAnchor = updateLine === "@@" || updateLine.startsWith("@@ ");
|
|
136
|
+
if (isAnchor && current && current.oldLines.length === 0 && current.newLines.length === 0)
|
|
137
|
+
throw new PatchParseError(UNEXPECTED(line), number);
|
|
138
|
+
if (isAnchor) {
|
|
139
|
+
update.chunks.push({
|
|
140
|
+
...(updateLine === "@@" ? {} : { anchor: updateLine.slice(3) }),
|
|
141
|
+
oldLines: [],
|
|
142
|
+
newLines: [],
|
|
143
|
+
context: [],
|
|
144
|
+
endOfFile: false,
|
|
145
|
+
});
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (updateLine === EOF_MARKER) {
|
|
149
|
+
if (!current || (current.oldLines.length === 0 && current.newLines.length === 0))
|
|
150
|
+
throw new PatchParseError("Update hunk does not contain any lines", number);
|
|
151
|
+
current.endOfFile = true;
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (line === "" || line.startsWith(" ")) {
|
|
155
|
+
const target = chunk(update);
|
|
156
|
+
target.context.push([target.oldLines.length, target.newLines.length]);
|
|
157
|
+
const text = line === "" ? "" : line.slice(1);
|
|
158
|
+
target.oldLines.push(text);
|
|
159
|
+
target.newLines.push(text);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (line.startsWith("+")) {
|
|
163
|
+
chunk(update).newLines.push(line.slice(1));
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (line.startsWith("-")) {
|
|
167
|
+
chunk(update).oldLines.push(line.slice(1));
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (current && (current.oldLines.length > 0 || current.newLines.length > 0))
|
|
171
|
+
throw new PatchParseError(`Expected update hunk to start with a @@ context marker, got: '${line}'`, number);
|
|
172
|
+
throw new PatchParseError(UNEXPECTED(line), number);
|
|
173
|
+
});
|
|
174
|
+
// `mode` changes inside the callback above, which the compiler cannot see.
|
|
175
|
+
if (mode !== "ended") throw new PatchParseError(`The last line of the patch must be '${END}'`);
|
|
176
|
+
return hunks;
|
|
177
|
+
}
|