Let the claude harness reach the turn boundary at all
F25. rotationTick returned early for claude before reaching federatedTurn, which has one call site below that return. On the harness both burn-in runs used, no phase request could ever be read and every human decision recorded against a live session went undelivered. Claude still skips the occupancy state machine below, because it owns its context rollover through the installed hook. A turn boundary is not a rotation. F26. The phase brief listed every domain-legal target, so run 4's frame session read "research, implement" and asked for implement, which the project's path refuses. The path is Orchestra's to know: the brief now names one step and says a wrong target comes back with the right one. F27. A refused request only reached recordError, leaving the agent to rewrite the same rejected file forever with nothing telling it why. federation. StatusError makes a 409 classifiable, and the refusal is delivered through sendPrompt under the F20 guarantee. A transport failure is not an answer: the request survives and the agent is told nothing. The F25 regression test fails against the unfixed rotationTick. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011xsXyr5J1RACo71YeKG3Pu
This commit is contained in:
@@ -117,13 +117,12 @@ func phaseRequestBrief(phase domain.WorkPhase) string {
|
||||
var b strings.Builder
|
||||
b.WriteString("\nAsk by writing .orchestra/phase-request.json at the end of a turn:\n\n")
|
||||
fmt.Fprintf(&b, " {\"from\": %q, \"to\": %q}\n", string(phase), string(next[0]))
|
||||
if len(next) > 1 {
|
||||
var names []string
|
||||
for _, p := range next {
|
||||
names = append(names, string(p))
|
||||
}
|
||||
fmt.Fprintf(&b, "\nLegal values for \"to\" from here: %s. This project may allow fewer, and a request outside its path is refused with the phase you may ask for.\n", strings.Join(names, ", "))
|
||||
}
|
||||
// Only one target is named. Listing every domain-legal move invited the
|
||||
// agent to skip ahead: run 4's frame session read "research, implement"
|
||||
// and asked for implement, which the project's path refuses. The path is
|
||||
// Orchestra's to know, so the brief states one step and says a wrong
|
||||
// target comes back with the right one.
|
||||
b.WriteString("\nAsk for one step. A request the project's path does not allow is refused, and the refusal names the phase you may ask for.\n")
|
||||
if artifact := phaseSealFile[phase]; artifact != "" {
|
||||
fmt.Fprintf(&b, "\nSeal .orchestra/%s before you ask. The request is refused without it.\n", artifact)
|
||||
}
|
||||
|
||||
@@ -81,11 +81,23 @@ func (c Client) request(ctx context.Context, method, path string, body any) (*ht
|
||||
if resp.StatusCode/100 != 2 {
|
||||
defer resp.Body.Close()
|
||||
b, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("federation: %s: %s", resp.Status, strings.TrimSpace(string(b)))
|
||||
return nil, &StatusError{Code: resp.StatusCode, Status: resp.Status, Body: strings.TrimSpace(string(b))}
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// StatusError is a coordinator answer the caller can classify. A refusal is
|
||||
// the agent's mistake and has to reach the agent; a transport failure is not,
|
||||
// and must not be reported to it as one. The message keeps the previous
|
||||
// wording so callers that match on it still work.
|
||||
type StatusError struct {
|
||||
Code int
|
||||
Status string
|
||||
Body string
|
||||
}
|
||||
|
||||
func (e *StatusError) Error() string { return fmt.Sprintf("federation: %s: %s", e.Status, e.Body) }
|
||||
|
||||
func (c Client) Events(ctx context.Context, since uint64) ([]domain.Event, uint64, error) {
|
||||
resp, err := c.request(ctx, http.MethodGet, "/v1/federation/events?since="+fmt.Sprint(since), nil)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user