mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-21 10:43:36 +00:00
Preserve Patrol assessment action kind
This commit is contained in:
@@ -330,8 +330,9 @@ runtime cost control, and shared AI transport surfaces.
|
||||
and Patrol run ID, safe run type/status/runtime-failure flags, resource and
|
||||
action counts, a primary resource label, last-known approval/action status,
|
||||
risk level, timestamp, and Patrol assessment recommended next-step/action
|
||||
labels only when they can be safely extracted from the stored assessment
|
||||
handoff, but it must not expose model-only handoff text,
|
||||
labels plus the safe recommendation action kind only when they can be
|
||||
safely extracted from the stored assessment handoff, but it must not expose
|
||||
model-only handoff text,
|
||||
runtime failure detail, action preflight/result bodies, remediation
|
||||
descriptions, raw commands, or approval command payloads. Its
|
||||
`requires_approval` field is a current operator-decision flag only: pending
|
||||
@@ -359,8 +360,9 @@ runtime cost control, and shared AI transport surfaces.
|
||||
opening the session picker instead of presenting mount-time cached
|
||||
summaries as the operator's decision surface. For restored Patrol
|
||||
assessment sessions, that picker must present the safe recommended
|
||||
next-step/action label from `handoff_summary` when one is available instead
|
||||
of reducing the saved session to generic assessment context.
|
||||
next-step/action label from `handoff_summary` when one is available and
|
||||
restore the safe recommendation action kind as context metadata instead of
|
||||
reducing the saved session to generic assessment context.
|
||||
Browser-originated `handoff_context`, `handoff_resources`, and
|
||||
`handoff_actions` plus safe `handoff_metadata` are one-shot request seeds
|
||||
for the first successful chat turn. After that send succeeds, the drawer
|
||||
|
||||
@@ -2007,8 +2007,8 @@ carry private Assistant model-context metadata, but that payload is a safe
|
||||
reload marker only. It may carry the handoff kind, finding ID, Patrol run ID,
|
||||
safe run type/status/runtime-failure flags, counts, primary-resource label,
|
||||
last-known approval/action status, risk level, Patrol assessment recommended
|
||||
next-step/action labels when safely extractable from the stored assessment
|
||||
handoff, and summary timestamp; it must
|
||||
next-step/action labels plus the safe recommendation action kind when safely
|
||||
extractable from the stored assessment handoff, and summary timestamp; it must
|
||||
not serialize the model-only `handoff_context`, runtime failure detail, action
|
||||
preflight/result bodies, remediation descriptions, raw commands, or approval
|
||||
command payloads.
|
||||
|
||||
@@ -204,9 +204,10 @@ work extends shared components instead of creating new local variants.
|
||||
it must not infer a finding target from bounded action references or
|
||||
reconstruct hidden model context, provider details, retry payloads, commands,
|
||||
preflight output, or action results in the browser. If the safe summary
|
||||
includes a Patrol assessment recommended next step or action label, the
|
||||
session picker plus restored drawer briefing and action copy must use that
|
||||
recommendation rather than falling back to generic assessment copy.
|
||||
includes a Patrol assessment recommended next step, action label, or safe
|
||||
action kind, the session picker plus restored drawer briefing and action
|
||||
copy must use that recommendation and may carry the safe action kind as
|
||||
context metadata rather than falling back to generic assessment copy.
|
||||
Session-load and new-conversation transitions must be success-bound: if the
|
||||
underlying session operation fails, the shared drawer store must not clear or
|
||||
replace the current scoped handoff context.
|
||||
|
||||
@@ -52,6 +52,7 @@ describe('AIChatAPI', () => {
|
||||
last_known_action_risk: 'high',
|
||||
recommended_next_step: 'Review pending Patrol approval',
|
||||
recommended_next_step_action: 'Review approvals',
|
||||
recommended_next_step_action_kind: 'review_approvals',
|
||||
updated_at: '2026-05-06T12:08:00Z',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -55,6 +55,7 @@ export interface ChatSessionHandoffSummary {
|
||||
last_known_action_risk?: string;
|
||||
recommended_next_step?: string;
|
||||
recommended_next_step_action?: string;
|
||||
recommended_next_step_action_kind?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -889,6 +889,7 @@ describe('AIChat', () => {
|
||||
requires_approval: false,
|
||||
recommended_next_step: 'Verify full coverage',
|
||||
recommended_next_step_action: 'Run Patrol',
|
||||
recommended_next_step_action_kind: 'run_patrol',
|
||||
},
|
||||
},
|
||||
]);
|
||||
@@ -919,6 +920,7 @@ describe('AIChat', () => {
|
||||
findingId: undefined,
|
||||
recommendedNextStep: 'Verify full coverage',
|
||||
recommendedNextStepAction: 'Run Patrol',
|
||||
recommendedNextStepActionKind: 'run_patrol',
|
||||
}),
|
||||
briefing: expect.objectContaining({
|
||||
sourceLabel: 'Pulse Patrol',
|
||||
|
||||
@@ -163,6 +163,7 @@ const buildSessionHandoffContext = (session?: ChatSession): AIChatContext | unde
|
||||
const statusLabel = formatSessionHandoffStatus(summary);
|
||||
const recommendedNextStep = summary.recommended_next_step?.trim() || '';
|
||||
const recommendedNextStepAction = summary.recommended_next_step_action?.trim() || '';
|
||||
const recommendedNextStepActionKind = summary.recommended_next_step_action_kind?.trim() || '';
|
||||
const actionCount = summary.action_count ?? 0;
|
||||
const resourceCount = summary.resource_count ?? 0;
|
||||
const findingId = summary.finding_id?.trim() || undefined;
|
||||
@@ -223,6 +224,7 @@ const buildSessionHandoffContext = (session?: ChatSession): AIChatContext | unde
|
||||
lastKnownActionRisk: summary.last_known_action_risk,
|
||||
recommendedNextStep,
|
||||
recommendedNextStepAction,
|
||||
recommendedNextStepActionKind,
|
||||
updatedAt: summary.updated_at,
|
||||
},
|
||||
findingId,
|
||||
|
||||
+34
-29
@@ -223,20 +223,21 @@ func inferPatrolRunHandoffMetadata(handoffContext string) HandoffMetadata {
|
||||
return NormalizeHandoffMetadata(metadata)
|
||||
}
|
||||
|
||||
func patrolAssessmentRecommendedNextStepSummary(kind string, handoffContext string) (string, string) {
|
||||
func patrolAssessmentRecommendedNextStepSummary(kind string, handoffContext string) (string, string, string) {
|
||||
if kind != sessionHandoffKindPatrolAssessment {
|
||||
return "", ""
|
||||
return "", "", ""
|
||||
}
|
||||
|
||||
lines := strings.Split(strings.TrimSpace(handoffContext), "\n")
|
||||
if len(lines) == 0 {
|
||||
return "", ""
|
||||
return "", "", ""
|
||||
}
|
||||
|
||||
sawAssessmentContext := false
|
||||
sawPatrolSource := false
|
||||
var recommendedNextStep string
|
||||
var recommendedNextStepAction string
|
||||
var recommendedNextStepActionKind string
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "[Patrol Assessment Context]" {
|
||||
@@ -261,36 +262,39 @@ func patrolAssessmentRecommendedNextStepSummary(kind string, handoffContext stri
|
||||
case "recommended next step":
|
||||
recommendedNextStep = safeSessionHandoffSummaryText(value, 160)
|
||||
case "recommended next step action":
|
||||
recommendedNextStepAction = safePatrolAssessmentRecommendationAction(value)
|
||||
recommendedNextStepAction, recommendedNextStepActionKind = safePatrolAssessmentRecommendationAction(value)
|
||||
}
|
||||
}
|
||||
|
||||
if !sawAssessmentContext || !sawPatrolSource {
|
||||
return "", ""
|
||||
return "", "", ""
|
||||
}
|
||||
if recommendedNextStep == "" && recommendedNextStepAction != "" {
|
||||
recommendedNextStep = recommendedNextStepAction
|
||||
}
|
||||
return recommendedNextStep, recommendedNextStepAction
|
||||
return recommendedNextStep, recommendedNextStepAction, recommendedNextStepActionKind
|
||||
}
|
||||
|
||||
func safePatrolAssessmentRecommendationAction(value string) string {
|
||||
func safePatrolAssessmentRecommendationAction(value string) (string, string) {
|
||||
action := safeSessionHandoffSummaryText(value, 120)
|
||||
if action == "" {
|
||||
return ""
|
||||
return "", ""
|
||||
}
|
||||
for _, suffix := range []string{
|
||||
" (discuss_assessment)",
|
||||
" (open_provider_settings)",
|
||||
" (review_approvals)",
|
||||
" (review_findings)",
|
||||
" (run_patrol)",
|
||||
for _, candidate := range []struct {
|
||||
suffix string
|
||||
kind string
|
||||
}{
|
||||
{" (discuss_assessment)", "discuss_assessment"},
|
||||
{" (open_provider_settings)", "open_provider_settings"},
|
||||
{" (review_approvals)", "review_approvals"},
|
||||
{" (review_findings)", "review_findings"},
|
||||
{" (run_patrol)", "run_patrol"},
|
||||
} {
|
||||
if strings.HasSuffix(action, suffix) {
|
||||
return strings.TrimSpace(strings.TrimSuffix(action, suffix))
|
||||
if strings.HasSuffix(action, candidate.suffix) {
|
||||
return strings.TrimSpace(strings.TrimSuffix(action, candidate.suffix)), candidate.kind
|
||||
}
|
||||
}
|
||||
return action
|
||||
return action, ""
|
||||
}
|
||||
|
||||
func safeSessionHandoffSummaryText(value string, maxRunes int) string {
|
||||
@@ -396,23 +400,24 @@ func modelContextHandoffSummary(modelContext *sessionModelContext) *SessionHando
|
||||
} else if findingID != "" {
|
||||
kind = sessionHandoffKindPatrolFinding
|
||||
}
|
||||
recommendedNextStep, recommendedNextStepAction := patrolAssessmentRecommendedNextStepSummary(
|
||||
recommendedNextStep, recommendedNextStepAction, recommendedNextStepActionKind := patrolAssessmentRecommendedNextStepSummary(
|
||||
kind,
|
||||
modelContext.HandoffContext,
|
||||
)
|
||||
|
||||
summary := &SessionHandoffSummary{
|
||||
Kind: kind,
|
||||
FindingID: findingID,
|
||||
RunID: metadata.RunID,
|
||||
RunType: metadata.RunType,
|
||||
RunStatus: metadata.RunStatus,
|
||||
RuntimeFailure: metadata.RuntimeFailure,
|
||||
HasModelContext: strings.TrimSpace(modelContext.HandoffContext) != "",
|
||||
ResourceCount: len(resources),
|
||||
ActionCount: len(actions),
|
||||
RecommendedNextStep: recommendedNextStep,
|
||||
RecommendedNextStepAction: recommendedNextStepAction,
|
||||
Kind: kind,
|
||||
FindingID: findingID,
|
||||
RunID: metadata.RunID,
|
||||
RunType: metadata.RunType,
|
||||
RunStatus: metadata.RunStatus,
|
||||
RuntimeFailure: metadata.RuntimeFailure,
|
||||
HasModelContext: strings.TrimSpace(modelContext.HandoffContext) != "",
|
||||
ResourceCount: len(resources),
|
||||
ActionCount: len(actions),
|
||||
RecommendedNextStep: recommendedNextStep,
|
||||
RecommendedNextStepAction: recommendedNextStepAction,
|
||||
RecommendedNextStepActionKind: recommendedNextStepActionKind,
|
||||
}
|
||||
if kind != sessionHandoffKindPatrolRun {
|
||||
summary.RunID = ""
|
||||
|
||||
@@ -567,7 +567,9 @@ func TestSessionStore_ListKeepsPatrolAssessmentHandoffIdentity(t *testing.T) {
|
||||
if summary.LastKnownApprovalStatus != "pending" || summary.LastKnownActionRisk != "high" {
|
||||
t.Fatalf("approval posture = %#v, want safe action summary", summary)
|
||||
}
|
||||
if summary.RecommendedNextStep != "Verify full coverage" || summary.RecommendedNextStepAction != "Run Patrol" {
|
||||
if summary.RecommendedNextStep != "Verify full coverage" ||
|
||||
summary.RecommendedNextStepAction != "Run Patrol" ||
|
||||
summary.RecommendedNextStepActionKind != "run_patrol" {
|
||||
t.Fatalf("recommended next step summary = %#v, want safe Patrol recommendation", summary)
|
||||
}
|
||||
payload, err := json.Marshal(sessions)
|
||||
@@ -618,7 +620,9 @@ func TestSessionStore_ListWithholdsUnsafePatrolAssessmentRecommendationSummary(t
|
||||
}
|
||||
|
||||
summary := sessions[0].HandoffSummary
|
||||
if summary.RecommendedNextStep != "" || summary.RecommendedNextStepAction != "" {
|
||||
if summary.RecommendedNextStep != "" ||
|
||||
summary.RecommendedNextStepAction != "" ||
|
||||
summary.RecommendedNextStepActionKind != "" {
|
||||
t.Fatalf("unsafe recommendation summary = %#v, want withheld recommendation fields", summary)
|
||||
}
|
||||
}
|
||||
|
||||
+18
-17
@@ -25,23 +25,24 @@ type Session struct {
|
||||
// session-scoped model handoff metadata. It intentionally omits model-only
|
||||
// context text and raw remediation details.
|
||||
type SessionHandoffSummary struct {
|
||||
Kind string `json:"kind,omitempty"`
|
||||
FindingID string `json:"finding_id,omitempty"`
|
||||
RunID string `json:"run_id,omitempty"`
|
||||
RunType string `json:"run_type,omitempty"`
|
||||
RunStatus string `json:"run_status,omitempty"`
|
||||
RuntimeFailure bool `json:"runtime_failure,omitempty"`
|
||||
HasModelContext bool `json:"has_model_context"`
|
||||
ResourceCount int `json:"resource_count,omitempty"`
|
||||
PrimaryResource *HandoffResource `json:"primary_resource,omitempty"`
|
||||
ActionCount int `json:"action_count,omitempty"`
|
||||
RequiresApproval bool `json:"requires_approval,omitempty"`
|
||||
LastKnownApprovalStatus string `json:"last_known_approval_status,omitempty"`
|
||||
LastKnownActionState string `json:"last_known_action_state,omitempty"`
|
||||
LastKnownActionRisk string `json:"last_known_action_risk,omitempty"`
|
||||
RecommendedNextStep string `json:"recommended_next_step,omitempty"`
|
||||
RecommendedNextStepAction string `json:"recommended_next_step_action,omitempty"`
|
||||
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
||||
Kind string `json:"kind,omitempty"`
|
||||
FindingID string `json:"finding_id,omitempty"`
|
||||
RunID string `json:"run_id,omitempty"`
|
||||
RunType string `json:"run_type,omitempty"`
|
||||
RunStatus string `json:"run_status,omitempty"`
|
||||
RuntimeFailure bool `json:"runtime_failure,omitempty"`
|
||||
HasModelContext bool `json:"has_model_context"`
|
||||
ResourceCount int `json:"resource_count,omitempty"`
|
||||
PrimaryResource *HandoffResource `json:"primary_resource,omitempty"`
|
||||
ActionCount int `json:"action_count,omitempty"`
|
||||
RequiresApproval bool `json:"requires_approval,omitempty"`
|
||||
LastKnownApprovalStatus string `json:"last_known_approval_status,omitempty"`
|
||||
LastKnownActionState string `json:"last_known_action_state,omitempty"`
|
||||
LastKnownActionRisk string `json:"last_known_action_risk,omitempty"`
|
||||
RecommendedNextStep string `json:"recommended_next_step,omitempty"`
|
||||
RecommendedNextStepAction string `json:"recommended_next_step_action,omitempty"`
|
||||
RecommendedNextStepActionKind string `json:"recommended_next_step_action_kind,omitempty"`
|
||||
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
||||
}
|
||||
|
||||
// Message represents a chat message
|
||||
|
||||
Reference in New Issue
Block a user