feat(platform): track last_outbound_at for silent-workspace detection (closes #817)
Sub of #795 (phantom-busy post-mortem). Adds last_outbound_at TIMESTAMPTZ column to workspaces. Bumped async on every successful outbound A2A call from a real workspace (skip canvas + system callers). Exposed in GET /workspaces/:id response as "last_outbound_at". PM/Dev Lead orchestrators can now detect workspaces that have gone silent despite being online (> 2h + active cron = phantom-busy warning). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0d538ab27a
commit
2f36bb9a7f
@ -591,6 +591,20 @@ func (h *WorkspaceHandler) logA2ASuccess(ctx context.Context, workspaceID, calle
|
|||||||
if wsNameForLog == "" {
|
if wsNameForLog == "" {
|
||||||
wsNameForLog = workspaceID
|
wsNameForLog = workspaceID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #817: track outbound activity on the CALLER so orchestrators can detect
|
||||||
|
// silent workspaces. Only update when callerID is a real workspace (not
|
||||||
|
// canvas, not a system caller) and the target returned 2xx/3xx.
|
||||||
|
if callerID != "" && !isSystemCaller(callerID) && statusCode < 400 {
|
||||||
|
go func() {
|
||||||
|
bgCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if _, err := db.DB.ExecContext(bgCtx,
|
||||||
|
`UPDATE workspaces SET last_outbound_at = NOW() WHERE id = $1`, callerID); err != nil {
|
||||||
|
log.Printf("last_outbound_at update failed for %s: %v", callerID, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
summary := a2aMethod + " → " + wsNameForLog
|
summary := a2aMethod + " → " + wsNameForLog
|
||||||
go func(parent context.Context) {
|
go func(parent context.Context) {
|
||||||
logCtx, cancel := context.WithTimeout(context.WithoutCancel(parent), 30*time.Second)
|
logCtx, cancel := context.WithTimeout(context.WithoutCancel(parent), 30*time.Second)
|
||||||
|
|||||||
@ -442,6 +442,18 @@ func (h *WorkspaceHandler) Get(c *gin.Context) {
|
|||||||
delete(ws, "budget_limit")
|
delete(ws, "budget_limit")
|
||||||
delete(ws, "monthly_spend")
|
delete(ws, "monthly_spend")
|
||||||
|
|
||||||
|
// #817: expose last_outbound_at so orchestrators can detect silent
|
||||||
|
// workspaces. Non-sensitive — just a timestamp of the most recent
|
||||||
|
// outbound A2A. Null if the workspace has never sent anything.
|
||||||
|
var lastOutbound sql.NullTime
|
||||||
|
if err := db.DB.QueryRowContext(c.Request.Context(),
|
||||||
|
`SELECT last_outbound_at FROM workspaces WHERE id = $1`, id,
|
||||||
|
).Scan(&lastOutbound); err == nil && lastOutbound.Valid {
|
||||||
|
ws["last_outbound_at"] = lastOutbound.Time
|
||||||
|
} else {
|
||||||
|
ws["last_outbound_at"] = nil
|
||||||
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, ws)
|
c.JSON(http.StatusOK, ws)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,5 @@
|
|||||||
|
-- Issue #817 (sub of #795): track last outbound A2A activity per workspace so
|
||||||
|
-- PM/Dev Lead can detect workspaces that have gone silent despite being online.
|
||||||
|
-- The orchestrator compares this against now() in its pulse; > 2 hours with an
|
||||||
|
-- active cron triggers a phantom-busy warning.
|
||||||
|
ALTER TABLE workspaces ADD COLUMN IF NOT EXISTS last_outbound_at TIMESTAMPTZ;
|
||||||
Loading…
Reference in New Issue
Block a user