From 864a930b55a6d458dde00f6c39e9fc5895da3f88 Mon Sep 17 00:00:00 2001 From: toki Date: Sat, 15 Aug 2026 08:15:15 +0900 Subject: [PATCH] =?UTF-8?q?fix(openai):=20=EB=B0=9C=EA=B8=89=EB=90=9C=20ar?= =?UTF-8?q?tifact=20=EA=B2=BD=EB=A1=9C=EB=A5=BC=20=EC=A0=95=EA=B7=9C?= =?UTF-8?q?=ED=99=94=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 로컬 모델이 선행 점을 슬래시로 바꾼 경우에도 정확한 Plan과 Review만 식별하고 일반 workspace 호출은 그대로 유지한다. --- apps/edge/internal/openai/hot_path_light.go | 29 ++++++++++++++++++- .../internal/openai/hot_path_light_test.go | 18 ++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/apps/edge/internal/openai/hot_path_light.go b/apps/edge/internal/openai/hot_path_light.go index 48b6a16a..de559e6b 100644 --- a/apps/edge/internal/openai/hot_path_light.go +++ b/apps/edge/internal/openai/hot_path_light.go @@ -5,6 +5,8 @@ import ( "encoding/json" "fmt" "net/http" + "path/filepath" + "sort" "strconv" "strings" "sync" @@ -596,7 +598,7 @@ func hotPathToolAllowed(tools []any, name string) bool { } func hotPathWorkspaceCall(kind hotPathPendingKind, paths reservedPaths, call normalizedToolCall) (workspaceOperationKind, string, bool, error) { - reserved := reservedPathsFromToolCall(call) + reserved := workspaceArtifactPathsFromToolCall(call, paths) if len(reserved) != 1 { return "", "", false, nil } @@ -618,6 +620,31 @@ func hotPathWorkspaceCall(kind hotPathPendingKind, paths reservedPaths, call nor return "", "", false, nil } +func workspaceArtifactPathsFromToolCall(call normalizedToolCall, paths reservedPaths) []string { + set := make(map[string]struct{}) + for _, path := range reservedPathsFromToolCall(call) { + set[cleanRelativePath(path)] = struct{}{} + } + addAlias := func(value string) { + normalized := filepath.ToSlash(strings.TrimSpace(value)) + if strings.HasPrefix(normalized, "/iop/job/") { + normalized = "." + strings.TrimPrefix(normalized, "/") + } + cleaned := cleanRelativePath(normalized) + if cleaned == cleanRelativePath(paths.PlanPath) || cleaned == cleanRelativePath(paths.ReviewPath) { + set[cleaned] = struct{}{} + } + } + addAlias(call.Path) + collectReservedStrings(call.Arguments, addAlias) + result := make([]string, 0, len(set)) + for path := range set { + result = append(result, path) + } + sort.Strings(result) + return result +} + func (s *hotPathLightStore) consumeChat(ownerEdgeID, principalRef string, rawBody []byte, lineage logicalRequestContinuationLineage, coordinator *logicalRequestCoordinator) (logicalRequestSnapshot, hotPathLightDisposition, bool, error) { results, err := decodeChatWorkspaceResults(rawBody) if err != nil { diff --git a/apps/edge/internal/openai/hot_path_light_test.go b/apps/edge/internal/openai/hot_path_light_test.go index 9c433d28..b5d00e30 100644 --- a/apps/edge/internal/openai/hot_path_light_test.go +++ b/apps/edge/internal/openai/hot_path_light_test.go @@ -76,6 +76,24 @@ func TestHotPathStageOrdinaryWorkspacePathPassesThrough(t *testing.T) { } } +func TestHotPathStageIssuedPlanSlashAliasMapsToCallerTool(t *testing.T) { + binding := mustBinding(t, fullWorkspaceAlternative("command", "bash", true), []any{openAIChatTool("bash", commandSchema())}) + record := &hotPathLightRecord{ + requestID: "req_stage_alias", phase: hotPathPhaseLocalActive, binding: binding, + } + coordinator := newLogicalRequestCoordinator(logicalRequestCoordinatorOptions{IDSource: func() (string, error) { return "call_public", nil }}) + alias := "/" + strings.TrimPrefix(newReservedPaths(record.requestID).PlanPath, ".") + mapped, pending, err := mapHotPathStageCalls(record, normalizedStageOutput{ToolCalls: []normalizedToolCall{{ + ID: "provider_read", Name: "read", Arguments: map[string]any{"filePath": alias}, + }}}, hotPathPendingLocalTools, coordinator, nil) + if err != nil { + t.Fatal(err) + } + if len(mapped.ToolCalls) != 1 || mapped.ToolCalls[0].Name != "bash" || pending[mapped.ToolCalls[0].ID].payload == nil { + t.Fatalf("issued plan alias was not normalized: calls=%+v pending=%+v", mapped.ToolCalls, pending) + } +} + func TestWorkerProviderReceivesOnlyCallerTools(t *testing.T) { callerTools := []any{openAIChatTool("run_command", commandSchema())} tools := hotPathStageProviderTools(hotPathDispatchSnapshot{Phase: hotPathPhaseLocalActive, Tools: callerTools})