@agentproto/apps 0.21.0 → 0.21.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentproto/apps",
3
- "version": "0.21.0",
3
+ "version": "0.21.1",
4
4
  "description": "@agentproto/apps — a home for ready-made agentproto apps (teams of agents + their workflows), declared with @agentproto/app-kit. Import a team (code-team, content-team, …) and use any of its agents/workflows from your own host: in-process via toMastraAgents(), or emit its manifests to disk.",
5
5
  "keywords": [
6
6
  "agentproto",
@@ -173,9 +173,9 @@
173
173
  },
174
174
  "dependencies": {
175
175
  "zod": "^4.6.5",
176
- "@agentproto/agent": "0.2.5",
177
176
  "@agentproto/app-client": "0.4.1",
178
- "@agentproto/app-kit": "1.7.0",
177
+ "@agentproto/app-kit": "1.7.1",
178
+ "@agentproto/agent": "0.2.5",
179
179
  "@agentproto/workflow": "0.7.1"
180
180
  },
181
181
  "peerDependencies": {
@@ -168,13 +168,21 @@ export function reviewableWorktrees(worktreeGcPlanResult) {
168
168
  const entries = Array.isArray(worktreeGcPlanResult?.plan) ? worktreeGcPlanResult.plan : []
169
169
  for (const w of entries) {
170
170
  if (w?.class !== "hold" || !w.branch || !w.path) continue
171
- if (w.liveness?.state !== "idle" || w.tree?.state !== "clean") continue
171
+ if (w.liveness?.state !== "idle" || treeState(w.tree) !== "clean") continue
172
172
  if (w.integration?.state === "open") continue
173
173
  out.set(w.path, w.branch)
174
174
  }
175
175
  return out
176
176
  }
177
177
 
178
+ /** A worktree_gc plan entry's tree state. The daemon's `worktree_gc` tool
179
+ * flattens it to the bare discriminant (`tree: "clean"`, `toGcPlanEntryView`
180
+ * in packages/cli/src/commands/worktree.ts); the engine's own `GcPlanEntry`
181
+ * (and `agentproto worktree gc --json`) carries `{ state, … }`. */
182
+ function treeState(tree) {
183
+ return typeof tree === "string" ? tree : tree?.state
184
+ }
185
+
178
186
  /** The worktree path a branch_gc entry is held by, when that hold is the only
179
187
  * thing between it and `review`: held for `worktree`, unmerged, old enough,
180
188
  * and the worktree is one {@link reviewableWorktrees} kept. Else null. */
@@ -56,6 +56,24 @@ inputs:
56
56
  Ask low-confidence idle sessions directly whether they're done. Off by
57
57
  default — it spends a turn in someone else's conversation.
58
58
  default: false
59
+ recurring:
60
+ type: boolean
61
+ description: >-
62
+ Set by scheduled runs (the hourly routine). A recurring run closes
63
+ sessions nobody asked about, so a keepAlive session must also have been
64
+ idle `keepAliveAskAfterMinutes`. Default false = an on-demand run, which
65
+ applies no such delay.
66
+ default: false
67
+ keepAliveAskAfterMinutes:
68
+ type: number
69
+ description: >-
70
+ With `askSessions`, for `recurring` runs only: a keepAlive session whose
71
+ worktree is merged or clean (nothing uncommitted, nothing ahead of base)
72
+ is asked only once idle this many minutes; 0 disables the recurring
73
+ keepAlive ask. On-demand runs ignore it. keepAlive only re-lights a
74
+ session after a daemon restart; it does not stop a declared DONE from
75
+ closing it.
76
+ default: 1440
59
77
  callerSessionId:
60
78
  type: string
61
79
  description: The calling session's id — never a candidate.
@@ -342,7 +360,10 @@ steps:
342
360
  - id: askQueue
343
361
  kind: transform
344
362
  name: Low-confidence idle sessions to ask directly
345
- description: Entry-based. Empty unless `askSessions`.
363
+ description: >-
364
+ Entry-based. Empty unless `askSessions`. A keepAlive session is included
365
+ only with a merged/clean worktree, and on a `recurring` run only once
366
+ idle `keepAliveAskAfterMinutes` (default 1440, 0 = never).
346
367
 
347
368
  - id: ask
348
369
  kind: map
@@ -488,8 +509,16 @@ as observed, never nudged.
488
509
  - `session_wrapup_apply` re-classifies each id right before acting and always
489
510
  refuses `keep`-class ids; this workflow never feeds it one.
490
511
  - Rules only ever close `close`/`stuck` ids; a `keepAlive` session is never
491
- in those classes, so only a confident judge verdict (with `judgedBy`) can
492
- close it — as FIX-9A allows.
512
+ in those classes, so only a confident judge verdict or a declared DONE (with
513
+ `judgedBy`) can close it — as FIX-9A allows. `keepAlive` means "re-light
514
+ after a daemon restart", not "never close": with `askSessions`, a keepAlive
515
+ session whose worktree is merged or clean (no uncommitted change, nothing
516
+ ahead of base; unknown worktree is not clean) is asked like any other. Closing
517
+ is an active act, so an on-demand run (`recurring: false`, e.g. `agentproto
518
+ steward --ask-sessions`) applies no idle delay to it beyond `idleMinutes`; only
519
+ a `recurring` run waits `keepAliveAskAfterMinutes` (default 1440 = 24 h, 0
520
+ disables), so nobody wakes up to find a session closed that they meant to
521
+ resume. A steward close is a deliberate end, so the sentinel never revives it.
493
522
  - A malformed judge reply is `active` with confidence 0 — never acted on.
494
523
  - The caller's own session (`callerSessionId`) is dropped from every list.
495
524
  - `blocked` / `needs-input` only FLAG a session; it keeps running.
@@ -57,6 +57,11 @@ import {
57
57
 
58
58
  const DEFAULT_IDLE_MINUTES = 30
59
59
  const DEFAULT_MIN_CONFIDENCE = 0.8
60
+ /** Recurring (scheduled) runs only: a keepAlive session must have been idle
61
+ * this long before it may be asked, so a session its owner meant to pick up
62
+ * tomorrow is not closed overnight. 0 turns the recurring keepAlive ask off.
63
+ * An on-demand run (`recurring: false`) has no such delay. */
64
+ const DEFAULT_KEEPALIVE_ASK_AFTER_MINUTES = 1440
60
65
  /** Terminal sessions older than this (hours since they ended) are not listed
61
66
  * as relabel candidates. */
62
67
  const DEFAULT_RELABEL_WINDOW_HOURS = 24
@@ -118,6 +123,8 @@ export function resolveSettings(input, modelRoles) {
118
123
  jevModel: typeof i.jevModel === "string" && i.jevModel.trim() ? i.jevModel.trim() : DEFAULT_JEV_MODEL,
119
124
  maxJudged: Math.floor(num(i.maxJudged, DEFAULT_MAX_JUDGED)),
120
125
  askSessions: i.askSessions === true,
126
+ recurring: i.recurring === true,
127
+ keepAliveAskAfterMinutes: num(i.keepAliveAskAfterMinutes, DEFAULT_KEEPALIVE_ASK_AFTER_MINUTES, { min: 0 }),
121
128
  callerSessionId: typeof i.callerSessionId === "string" && i.callerSessionId ? i.callerSessionId : null,
122
129
  callerOrigin: typeof i.callerOrigin === "string" && i.callerOrigin ? i.callerOrigin : null,
123
130
  appId: typeof i.appId === "string" ? i.appId.trim() : DEFAULT_APP_ID,
@@ -427,8 +434,33 @@ export function collectVerdicts(evidenceResult, judgeQueue, jevResult, jevQueue,
427
434
 
428
435
  // ── ask (opt-in) ─────────────────────────────────────────────────────────
429
436
 
430
- /** Sessions to ask directly: judged below `minConfidence`, and idle, not
431
- * keepAlive, not awaitingInput — only when `askSessions`. */
437
+ /** Nothing to lose in the session's worktree: it is merged (the plan's signal
438
+ * or the PR state), or it is a linked worktree with no uncommitted changes
439
+ * and no commits beyond the base. No worktree info at all is unknown, not
440
+ * clean — a session in a plain checkout stays out. */
441
+ export function worktreeHasNothingToLose(evidence) {
442
+ if (evidence?.signals?.worktreeMerged === true) return true
443
+ const wt = evidence?.worktree
444
+ if (!wt) return false
445
+ if (wt.pr?.state === "merged") return true
446
+ return wt.dirty === false && wt.ahead === 0
447
+ }
448
+
449
+ /** keepAlive is the restart-relight flag; closing is a separate, active
450
+ * decision. A keepAlive session whose worktree holds nothing to lose may be
451
+ * asked. An on-demand run (a human asked for it) applies no idle delay beyond
452
+ * the plan's own `idleMinutes`. A `recurring` run closes things nobody asked
453
+ * about, so it also waits `keepAliveAskAfterMinutes` of idleness (0 = never). */
454
+ export function keepAliveAskEligible(evidence, settings) {
455
+ if (evidence?.keepAlive !== true || !worktreeHasNothingToLose(evidence)) return false
456
+ if (settings.recurring !== true) return true
457
+ const after = settings.keepAliveAskAfterMinutes
458
+ return after > 0 && typeof evidence.idleMinutes === "number" && evidence.idleMinutes >= after
459
+ }
460
+
461
+ /** Sessions to ask directly: judged below `minConfidence`, not awaitingInput
462
+ * or busy, and either not keepAlive or a keepAlive passing
463
+ * {@link keepAliveAskEligible} — only when `askSessions`. */
432
464
  export function buildAskQueue(verdicts, settings) {
433
465
  if (!settings?.askSessions) return []
434
466
  return (verdicts ?? [])
@@ -436,7 +468,7 @@ export function buildAskQueue(verdicts, settings) {
436
468
  r =>
437
469
  r.evidence &&
438
470
  r.confidence < settings.minConfidence &&
439
- r.evidence.keepAlive !== true &&
471
+ (r.evidence.keepAlive !== true || keepAliveAskEligible(r.evidence, settings)) &&
440
472
  r.evidence.awaitingInput !== true &&
441
473
  r.evidence.busy !== true,
442
474
  )
@@ -995,6 +1027,8 @@ export default {
995
1027
  judgeModel: { type: "string", description: `Model for the agent judge. Default: the \`${ROLE_JUDGE_SESSION}\` model role (repo agentproto.json \`models\` > daemon config \`models\` > built-in).` },
996
1028
  maxJudged: { type: "number", description: `Most \`judge\` sessions judged per run, most RAM first. Default ${DEFAULT_MAX_JUDGED}.`, default: DEFAULT_MAX_JUDGED },
997
1029
  askSessions: { type: "boolean", description: "Ask low-confidence idle sessions directly whether they're done. Default false — it spends a turn in someone else's conversation.", default: false },
1030
+ recurring: { type: "boolean", description: "Set by scheduled runs (the hourly routine). A recurring run closes sessions nobody asked about, so a keepAlive session must also have been idle `keepAliveAskAfterMinutes`. Default false = an on-demand run, which applies no such delay.", default: false },
1031
+ keepAliveAskAfterMinutes: { type: "number", description: `With \`askSessions\`, for \`recurring\` runs only: a keepAlive session whose worktree is merged or clean (nothing uncommitted, nothing ahead of base) is asked only once idle this many minutes; 0 disables the recurring keepAlive ask. On-demand runs ignore it. keepAlive only re-lights a session after a daemon restart; it does not stop a declared DONE from closing it. Default ${DEFAULT_KEEPALIVE_ASK_AFTER_MINUTES}.`, default: DEFAULT_KEEPALIVE_ASK_AFTER_MINUTES },
998
1032
  callerSessionId: { type: "string", description: "The calling session's id — never a candidate. The CLI passes AGENTPROTO_SESSION_ID." },
999
1033
  callerOrigin: { type: "string", description: "The calling session's origin (`cron:<jobId>`) — an older run of the SAME cron job is never judged as user work." },
1000
1034
  appId: { type: "string", description: `Installed app whose \`app_state\` ledger holds the verdict memory. Default ${DEFAULT_APP_ID}.` },
@@ -33,10 +33,18 @@ loader path can carry.
33
33
  judge session is released (killed + archived) when its item settles.
34
34
  Verdicts: `done|abandoned|blocked|needs-input|active`. A judge error never
35
35
  closes anything.
36
- 5. `ask` (only with `askSessions`) — low-confidence, idle, non-keepAlive,
37
- not-awaiting-input sessions get ONE prompt asking them to reply
36
+ 5. `ask` (only with `askSessions`) — low-confidence, idle, not-awaiting-input
37
+ sessions get ONE prompt asking them to reply
38
38
  `STEWARD: DONE …` / `STEWARD: NOT-DONE …`; a ~3 min bounded wait; the
39
- answer becomes a `declared` verdict.
39
+ answer becomes a `declared` verdict. `keepAlive` only re-lights a session
40
+ after a daemon restart, so it does not exempt one from the ask: a keepAlive
41
+ session whose worktree is merged or clean (nothing uncommitted, nothing
42
+ ahead of base; no worktree info counts as not clean) is asked too. Closing
43
+ is an active act: an on-demand run (`agentproto steward --ask-sessions`)
44
+ adds no idle delay beyond `--idle`. Only a scheduled run (`recurring: true`,
45
+ as the hourly routine sets) waits `keepAliveAskAfterMinutes` (default 1440,
46
+ 24 h; 0 turns it off), so a session you meant to resume is not closed
47
+ overnight.
40
48
  6. `judgedApply` (only with `apply`) — confident `done`/`abandoned` close
41
49
  (resumable, with a recorded outcome); confident `blocked`/`needs-input`
42
50
  only flag. Everything else is left alone and reported.
@@ -21,6 +21,9 @@ target:
21
21
  inputs:
22
22
  apply: true
23
23
  askSessions: false
24
+ # Scheduled run: if askSessions is ever turned on here, a keepAlive session
25
+ # is asked only after keepAliveAskAfterMinutes idle (default 1440, 0 = never).
26
+ recurring: true
24
27
  # Origin policy (the committed default): never close a human's session.
25
28
  # `chat-starter`/`vscode` (and any root with no origin and no parent) are
26
29
  # FLAG-ONLY; `cron:*` jobs, `gate`, `workflow` and `review` sessions, and executors (a session with