Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions scenarios/scrum-master/.env.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# OpenAI Configuration
# Use EITHER standard OpenAI OR Azure OpenAI (not both)

# Option 1: Standard OpenAI API
OPENAI_API_KEY=
OPENAI_MODEL=gpt-4o

# Option 2: Azure OpenAI (takes precedence if AZURE_OPENAI_API_KEY is set)
AZURE_OPENAI_API_KEY=
AZURE_OPENAI_ENDPOINT=
AZURE_OPENAI_DEPLOYMENT=
AZURE_OPENAI_API_VERSION=2024-10-21

# MCP Tooling Configuration
BEARER_TOKEN=

# Enable to use observability exporter, default is false which means using console exporter.
# MUST be true in any deployed/dev-tunnel environment or MAC Activity stays empty.
ENABLE_A365_OBSERVABILITY_EXPORTER=false
# Use by the sample to demo using custom token resolver and token cache when it is true, otherwise use the built-in AgenticTokenCache
Use_Custom_Resolver=true
# optional - set to enable observability logs, value can be 'info', 'warn', or 'error', default to 'none' if not set
A365_OBSERVABILITY_LOG_LEVEL=
# Uncomment together with A365_OBSERVABILITY_LOG_LEVEL to debug export failures.
# OTEL_LOG_LEVEL=INFO

# --- A365 observability identity ---
# Runtime agent identity (the AGENTIC INSTANCE, not the blueprint). Normally
# populated automatically from activity.recipient.agenticAppId. Set explicitly
# only if you invoke the observability helpers outside the standard A365
# hosting flow (e.g. a stand-alone script). See src/observability.ts.
agent365Observability__agentInstanceId=

# Environment Settings
NODE_ENV=development # Retrieve mcp servers from ToolingManifest
HOST=127.0.0.1 # Agents Playground connects to 127.0.0.1; set this so the server binds to the same address

# Telemetry and Tracing Configuration
DEBUG=agents:*

# --- Scenario-level logging (this sample only) -------------------------------
# Level-based logger in src/util/logger.ts.
# LOG_LEVEL: error | warn | info | debug | trace (default=info)
# LOG_HTTP: when 'true', traces instrumented axios requests (including Jira)
# with status + latency. Not Graph/fetch or MCP transport tracing.
# Credentials are redacted. Keep false in production.
LOG_LEVEL=info
LOG_HTTP=false

# Use Agentic Authentication rather than OBO
USE_AGENTIC_AUTH=false

# Service Connection Settings
connections__service_connection__settings__clientId=
connections__service_connection__settings__clientSecret=
connections__service_connection__settings__tenantId=

# Set service connection as default
connectionsMap__0__serviceUrl=*
connectionsMap__0__connection=service_connection

# AgenticAuthentication Options
agentic_type=agentic
agentic_altBlueprintConnectionName=service_connection
agentic_scopes=ea9ffc3e-8a23-4a7d-836d-234d7c7565c1/.default # Prod Agentic scope

# =============================================================================
# Scrum Master Assistant POC configuration (added on top of the base sample)
# =============================================================================

# --- Jira (Atlassian Cloud) ---
# Set JIRA_MODE=mock to run against the built-in mock sprint (no Atlassian creds needed).
JIRA_MODE=mock # live | mock
JIRA_BASE_URL=https://your-org.atlassian.net
JIRA_EMAIL=
JIRA_API_TOKEN=
JIRA_PROJECT_KEY=SCRUM # mock board key; replace for live Jira
JIRA_BOARD_ID=1

# --- SharePoint (Microsoft Graph, delegated) ---
# Site URL of the site that will host the SMA_* lists + SprintReports library.
SHAREPOINT_SITE_URL=
SHAREPOINT_LISTS_PREFIX=SMA_
# App registration used by scripts/setup-sharepoint.ts (device-code) and runtime Graph calls.
GRAPH_TENANT_ID=common
# Blank uses Microsoft Graph Command Line Tools public client
# (14d82eec-204b-4c2f-b7e8-296a70dab67e). Tenant policy/admin consent still applies.
# For production, register your own public client and set its app id here.
GRAPH_CLIENT_ID=

# --- Scrum ceremony scheduling ---
STANDUP_CRON=0 30 3 * * 1-5 # UTC = 09:00 Asia/Kolkata on weekdays
NIGHTLY_CRON=0 0 19 * * * # UTC = 00:30 Asia/Kolkata daily
Comment on lines +92 to +93

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should fix: Cron defaults are documented as UTC, but node-cron evaluates them in TIMEZONE, so the standup fires at 03:30 IST

The template describes these as UTC expressions (0 30 3 * * 1-5 # UTC = 09:00 Asia/Kolkata), and the README calls them 'UTC cron'. But the local scheduler passes { timezone: cfg.timezone } to node-cron, which matches the expression against wall-clock time in that zone.

With TIMEZONE=Asia/Kolkata:

  • The in-process standup fires at 03:30 IST on weekdays (22:00 UTC the previous evening).
  • With the 4-hour cutoff, the summary posts around 07:30 IST, before the team is online.
  • The nightly warn/report runs at 19:00 IST instead of 00:30.

config.ts uses the same UTC strings as defaults, so leaving the vars unset has the same problem. The local scheduler and the Azure Function timers (which run these strings as UTC NCRONTAB) also fire 5.5 hours apart, not 'simultaneously'. README.md:221 calls TIMEZONE 'Display only', but it drives scheduling, the T-2 gate in sprint-summary and the meeting-slot search.

Evidence
  • .env.template:88-89: STANDUP_CRON=0 30 3 * * 1-5 # UTC = 09:00 Asia/Kolkata on weekdays and NIGHTLY_CRON=0 0 19 * * * # UTC = 00:30 Asia/Kolkata daily.
  • .env.template:91: TIMEZONE=Asia/Kolkata. .env.template:98: LOCAL_CRON=true.
  • src/cron/local-scheduler.ts:37-44: cron.schedule(cfg.standupCron, async () => {...}, { timezone: cfg.timezone }); (same at :46-52 for nightly).
  • src/config.ts:106-109 defaults: '0 30 3 * * 1-5', '0 0 19 * * *', timezone: optional('TIMEZONE', 'Asia/Kolkata').
  • node-cron's timezone option compares against the time in that zone, so 0 30 3 * * 1-5 matches at 03:30 Asia/Kolkata.
  • Compare azure-functions/src/index.ts:31-34: // 09:00 Asia/Kolkata Mon–Fri = 03:30 UTC, schedule: '0 30 3 * * 1-5', as NCRONTAB in UTC.
  • README.md:218-219 say 'UTC cron — default = 09:00 IST weekdays'; README.md:221 says TIMEZONE is 'Display only'. Yet getScheduleConfig().timezone is used in local-scheduler.ts:44/52, sprint-summary.ts:253-263 and chase.ts:332.

Scenario: create .env from the template and run npm run dev on Monday. Standup cards are DM'd at 03:30 IST and the cutoff summary posts at about 07:30 IST.

With the Function App also deployed:

  1. The Function fires Mon 03:30 UTC (standupId 101#<Mon>).
  2. It summarizes and evicts the session (standup.ts:524).
  3. The local cron fires Mon 22:00 UTC with the same UTC-date standupId and fans out again.

Suggested fix: Pick one convention and make the template, config defaults and README agree:

  • Simplest: run node-cron in UTC ({ timezone: 'Etc/UTC' } in local-scheduler.ts). The documented values and the Function App timers then line up.
  • Or keep the TIMEZONE option and change the template, config defaults and README to 0 0 9 * * 1-5 and 0 30 0 * * *, described as 'evaluated in TIMEZONE'.

In either case, drop 'Display only' for TIMEZONE in the README.

Comment on lines +92 to +93

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should fix: Cron defaults are documented as UTC, but node-cron evaluates them in TIMEZONE, so the standup fires at 03:30 IST

The template describes these as UTC expressions (0 30 3 * * 1-5 # UTC = 09:00 Asia/Kolkata), and the README calls them 'UTC cron'. But the local scheduler passes { timezone: cfg.timezone } to node-cron, which matches the expression against wall-clock time in that zone.

With TIMEZONE=Asia/Kolkata:

  • The in-process standup fires at 03:30 IST on weekdays (22:00 UTC the previous evening).
  • With the 4-hour cutoff, the summary posts around 07:30 IST, before the team is online.
  • The nightly warn/report runs at 19:00 IST instead of 00:30.

config.ts uses the same UTC strings as defaults, so leaving the vars unset has the same problem. The local scheduler and the Azure Function timers (which run these strings as UTC NCRONTAB) also fire 5.5 hours apart, not 'simultaneously'. README.md:221 calls TIMEZONE 'Display only', but it drives scheduling, the T-2 gate in sprint-summary and the meeting-slot search.

Evidence
  • .env.template:88-89: STANDUP_CRON=0 30 3 * * 1-5 # UTC = 09:00 Asia/Kolkata on weekdays and NIGHTLY_CRON=0 0 19 * * * # UTC = 00:30 Asia/Kolkata daily.
  • .env.template:91: TIMEZONE=Asia/Kolkata. .env.template:98: LOCAL_CRON=true.
  • src/cron/local-scheduler.ts:37-44: cron.schedule(cfg.standupCron, async () => {...}, { timezone: cfg.timezone }); (same at :46-52 for nightly).
  • src/config.ts:106-109 defaults: '0 30 3 * * 1-5', '0 0 19 * * *', timezone: optional('TIMEZONE', 'Asia/Kolkata').
  • node-cron's timezone option compares against the time in that zone, so 0 30 3 * * 1-5 matches at 03:30 Asia/Kolkata.
  • Compare azure-functions/src/index.ts:31-34: // 09:00 Asia/Kolkata Mon–Fri = 03:30 UTC, schedule: '0 30 3 * * 1-5', as NCRONTAB in UTC.
  • README.md:218-219 say 'UTC cron — default = 09:00 IST weekdays'; README.md:221 says TIMEZONE is 'Display only'. Yet getScheduleConfig().timezone is used in local-scheduler.ts:44/52, sprint-summary.ts:253-263 and chase.ts:332.

Scenario: create .env from the template and run npm run dev on Monday. Standup cards are DM'd at 03:30 IST and the cutoff summary posts at about 07:30 IST.

With the Function App also deployed:

  1. The Function fires Mon 03:30 UTC (standupId 101#<Mon>).
  2. It summarizes and evicts the session (standup.ts:524).
  3. The local cron fires Mon 22:00 UTC with the same UTC-date standupId and fans out again.

Suggested fix: Pick one convention and make the template, config defaults and README agree:

  • Simplest: run node-cron in UTC ({ timezone: 'Etc/UTC' } in local-scheduler.ts). The documented values and the Function App timers then line up.
  • Or keep the TIMEZONE option and change the template, config defaults and README to 0 0 9 * * 1-5 and 0 30 0 * * *, described as 'evaluated in TIMEZONE'.

In either case, drop 'Display only' for TIMEZONE in the README.

Comment on lines +92 to +93

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should fix: Cron defaults are documented as UTC, but node-cron evaluates them in TIMEZONE, so the standup fires at 03:30 IST

The template describes these as UTC expressions (0 30 3 * * 1-5 # UTC = 09:00 Asia/Kolkata), and the README calls them 'UTC cron'. But the local scheduler passes { timezone: cfg.timezone } to node-cron, which matches the expression against wall-clock time in that zone.

With TIMEZONE=Asia/Kolkata:

  • The in-process standup fires at 03:30 IST on weekdays (22:00 UTC the previous evening).
  • With the 4-hour cutoff, the summary posts around 07:30 IST, before the team is online.
  • The nightly warn/report runs at 19:00 IST instead of 00:30.

config.ts uses the same UTC strings as defaults, so leaving the vars unset has the same problem. The local scheduler and the Azure Function timers (which run these strings as UTC NCRONTAB) also fire 5.5 hours apart, not 'simultaneously'. README.md:221 calls TIMEZONE 'Display only', but it drives scheduling, the T-2 gate in sprint-summary and the meeting-slot search.

Evidence
  • .env.template:88-89: STANDUP_CRON=0 30 3 * * 1-5 # UTC = 09:00 Asia/Kolkata on weekdays and NIGHTLY_CRON=0 0 19 * * * # UTC = 00:30 Asia/Kolkata daily.
  • .env.template:91: TIMEZONE=Asia/Kolkata. .env.template:98: LOCAL_CRON=true.
  • src/cron/local-scheduler.ts:37-44: cron.schedule(cfg.standupCron, async () => {...}, { timezone: cfg.timezone }); (same at :46-52 for nightly).
  • src/config.ts:106-109 defaults: '0 30 3 * * 1-5', '0 0 19 * * *', timezone: optional('TIMEZONE', 'Asia/Kolkata').
  • node-cron's timezone option compares against the time in that zone, so 0 30 3 * * 1-5 matches at 03:30 Asia/Kolkata.
  • Compare azure-functions/src/index.ts:31-34: // 09:00 Asia/Kolkata Mon–Fri = 03:30 UTC, schedule: '0 30 3 * * 1-5', as NCRONTAB in UTC.
  • README.md:218-219 say 'UTC cron — default = 09:00 IST weekdays'; README.md:221 says TIMEZONE is 'Display only'. Yet getScheduleConfig().timezone is used in local-scheduler.ts:44/52, sprint-summary.ts:253-263 and chase.ts:332.

Scenario: create .env from the template and run npm run dev on Monday. Standup cards are DM'd at 03:30 IST and the cutoff summary posts at about 07:30 IST.

With the Function App also deployed:

  1. The Function fires Mon 03:30 UTC (standupId 101#<Mon>).
  2. It summarizes and evicts the session (standup.ts:524).
  3. The local cron fires Mon 22:00 UTC with the same UTC-date standupId and fans out again.

Suggested fix: Pick one convention and make the template, config defaults and README agree:

  • Simplest: run node-cron in UTC ({ timezone: 'Etc/UTC' } in local-scheduler.ts). The documented values and the Function App timers then line up.
  • Or keep the TIMEZONE option and change the template, config defaults and README to 0 0 9 * * 1-5 and 0 30 0 * * *, described as 'evaluated in TIMEZONE'.

In either case, drop 'Display only' for TIMEZONE in the README.

STANDUP_CUTOFF_HOURS=4
TIMEZONE=Asia/Kolkata
# Both schedulers evaluate the expressions in UTC. TIMEZONE controls local
# date-based gates, card labels and meeting-slot searches, not cron evaluation.
# Leave WEBSITE_TIME_ZONE unset on the Functions app.

# --- Warn thresholds (Sprint risk detection) ---
WARN_TODO_PCT=0.40 # >40% of committed points still in To Do
WARN_SPRINT_PROGRESS_PCT=0.50 # ... once >=50% of sprint duration has elapsed

# --- Runtime toggles ---
LOCAL_CRON=true # run the in-process scheduler in dev; false in prod
AGENT_CALLBACK_URL= # Functions app setting: reachable agent base URL (no /api/messages suffix)
INTERNAL_TRIGGER_TOKEN= # required nonempty unless NODE_ENV is exactly development
# Cloud agent overrides: NODE_ENV=production, HOST=0.0.0.0, LOCAL_CRON=false
# when Functions timers are deployed. Do not copy the development values above.
35 changes: 35 additions & 0 deletions scenarios/scrum-master/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# dependencies + build
node_modules/
dist/
*.tsbuildinfo

# TS compile artefacts inside src (only source .ts should be tracked)
src/**/*.js
src/**/*.d.ts
src/**/*.js.map
azure-functions/src/**/*.js
azure-functions/src/**/*.d.ts
azure-functions/src/**/*.js.map

# env & secrets — never commit
.env
.env.local
.env.*.local
.mstoken-cache.json

# generated at first `npx a365` run — contains tenant/agent IDs
a365.generated.config.json
a365.config.json
.a365-workspace-detection.local.json

# dev logs
dev-out.txt
seed-out.txt
setup-out.txt
*.log

# agent skills installed via `gh skill add microsoft/agent365-skills` — workspace-local
.agents/
.claude/
.github/copilot-instructions.md
.github/copilot-instructions.md.bak
172 changes: 172 additions & 0 deletions scenarios/scrum-master/AGENT-CODE-WALKTHROUGH.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
# Scrum Master Agent Code Walkthrough

This walkthrough describes the scenario implementation, not the smaller base
OpenAI sample. For configuration and commands, see [`README.md`](README.md);
for flow diagrams, see [`docs/design.md`](docs/design.md).

## 1. Entry point and request authentication

[`src/index.ts`](src/index.ts) loads `.env`, installs scenario logging,
prints the startup configuration summary and creates the Express server.

- `GET /api/health` is public.
- `/api/internal/*` routes run **before** JWT middleware. They check the raw
`x-internal-token` header against `INTERNAL_TRIGGER_TOKEN`; this is a shared
secret, not an Agent 365 token exchange. Blank is allowed for local
development only when `NODE_ENV` is exactly `development`. Every other
value, including unset or `test`, requires a nonempty token at startup.
- `POST /api/messages` sits **after** `authorizeJWT(authConfig)`. Only
`NODE_ENV=development` uses the empty development auth configuration.
Other environments load the SDK connection settings.
- `CloudAdapter.process` converts the request into a turn and calls
`agentApplication.run(context)`. Request JWT validation and the agent's
downstream token acquisition are separate stages.

The listener uses `PORT` (default `3978`) and `HOST`. Cloud hosting must set
`NODE_ENV=production` and `HOST=0.0.0.0`; do not deploy the template's
loopback/development settings.

## 2. Agent application and dispatch

[`src/agent.ts`](src/agent.ts) defines `MyAgent`, an
`AgentApplication<TurnState>` using `MemoryStorage`. Its configured
authorization handler is `agentic`. Message and notification handlers declare
that handler; installation-update events have their own route.

For a message, the application:

1. Attempts observability token warm-up; failures are logged and do not block
the scenario turn.
2. Wraps dispatch in `runWithTurnBaggage`.
3. Best-effort captures the sender's conversation reference for proactive
delivery. Live mode stores it through the roster service; mock mode keeps
its roster in-memory.
4. Routes `activity.value.action` prefixes `standup.`, `reconcile.`,
`blocker.` and `meeting.` to the corresponding card handlers.
5. Routes recognized slash commands through `handlers/commands.ts`.
6. Sends nonempty free text to `handlers/answer.ts` by default.

The routing is important: ordinary sprint questions do **not** go through
`getClient()` and its generic MCP assistant.

## 3. Deterministic scenario handlers

| Handler | Responsibility |
|---|---|
| `handlers/standup.ts` | Group Jira issues by roster assignee, send cards, collect updates and summarize |
| `handlers/reconcile.ts` | Classify update phrases and apply/approve Jira transitions |
| `handlers/chase.ts` | Resolve blockers, propose slots and book a meeting after card actions |
| `handlers/warn.ts` | Evaluate sprint progress and remaining-point thresholds |
| `handlers/sprint-summary.ts` | Classify issue risk and post a mid-sprint RAG table |
| `handlers/report.ts` | Generate deterministic sprint-close markdown with a per-sprint delivery guard |
| `handlers/config.ts` | Capture the destination channel conversation reference |

Reconciliation approval cards submit only the standup id and approval
toggles. The handler requires the Scrum Master's AAD object id and uses a
server-side pending proposal with a 24-hour lifetime; caller-supplied issue
transitions are not authoritative. Apply/skip consumes the proposal.
Expired, replayed or post-restart submissions fail closed and need a new
proposal. The deterministic classifier refuses completion inference when
negation is present.

`/standup` and `/help` do not require LLM inference. In mock mode the first
personal Playground caller becomes the mock Alex (SM) persona and receives
Alex's issue card; other mock assignees are separate identities, not silently
merged into the caller. Live delivery requires real roster mappings and prior
conversations.

## 4. Grounded Jira Q&A

[`handlers/answer.ts`](src/handlers/answer.ts) creates a scenario-specific
`Agent` named `Scrum Master — Answer` using `JIRA_TOOLS` from
[`services/jira-tool.ts`](src/services/jira-tool.ts).

The model selects Jira tools for issue status, sprint issues and comments.
Friendly `Task-N` labels are translated at the tool boundary. `JIRA_MODE`
selects live REST or the mutable mock board; even with mocked Jira, inference
requires the configured OpenAI/Azure OpenAI service.

The handler retains at most eight user/assistant history items per user in a
process-local map. A restart loses this context. Empty model output receives
a fallback message; exceptions produce an error reply, not invented Jira data.

## 5. Generic MCP assistant and notifications

Setting `SMA_ANSWER_DISABLED=true` switches ordinary free text to the generic
client in [`src/client.ts`](src/client.ts). This path acknowledges the message,
sends typing activities and calls `getClient(authorization, authHandlerName,
turnContext, displayName)`.

`getClient()` creates an OpenAI agent with the configured model and registers
MCP servers through `McpToolRegistrationService`. Registration receives the
authorization object, handler name, turn context and optional `BEARER_TOKEN`.
It does not read `MCP_AUTH_TOKEN` or `AGENTIC_USER_ID`. If registration fails,
the warning is logged and the agent can continue without those tools; that is
not proof that an external operation succeeded.

The notification route handles email notifications by retrieving content,
then asking the generic client to summarize/draft a response while treating
the email body as untrusted data. Other notification types receive a generic
notification-type response. Installation updates send welcome/farewell text.

## 6. Calendar tools, fallback and connection lifecycle

[`services/calendar.ts`](src/services/calendar.ts) creates a separate
`Scrum Master — Calendar` agent with Zod-structured output and the same MCP
registration service pattern. It attempts calendar availability lookup and
event creation through the configured Calendar server.

- In mock mode the meeting-booking handler returns an offline notice rather
than attempting Graph-backed booking; it does not create a real event.
- If slot lookup or structured output is unusable, it returns synthetic
candidate slots. These are **not verified free/busy availability**.
- Booking failures are errors; suggested slots alone do not prove an event
was created. Successful booking requires the returned event details.
- The booking handler conditionally claims the blocker `open → booking`
before attempting Calendar and records `booked` afterwards. Uncertain
results retain a duplicate-prevention claim; inspect Outlook before
manually recovering it, rather than automatically retrying invitations.
- `withServers()` connects available transports and retries once on
`Session not found` / `-32001`.
- Neither the calendar helper nor `OpenAIClient.invokeAgent()` closes servers
after each call. The module-level tooling service reuses Streamable-HTTP
sessions; adding `finally { closeServers(); }` would break later turns.

## 7. Observability and logging

[`src/client.ts`](src/client.ts) configures the service name
`Scrum Master Sample Agent`, initializes `OpenAIAgentsTraceInstrumentor`,
and selects the custom token resolver or the built-in agentic token cache.
[`src/observability.ts`](src/observability.ts) supplies runtime identity,
baggage and invoke/tool scope helpers. Missing identity/token information may
skip observability without blocking the user flow.

The generic client wraps inference in `InferenceScope`, records input/output
messages and finish reasons, and disposes the scope in `finally`. It does
**not** record fabricated fixed token counts. Exporter configuration is not
evidence of successful Defender/MAC ingestion.

`LOG_LEVEL` centrally filters console output; SDK-specific logging may also
have separate controls. `LOG_HTTP=true` traces instrumented axios clients including Jira,
not Graph's fetch client or MCP transports.

## 8. Scheduling and persistence boundaries

[`cron/local-scheduler.ts`](src/cron/local-scheduler.ts) and the sibling
[`azure-functions/`](azure-functions) app drive standup and nightly ceremonies.
Both evaluate cron expressions in UTC; nightly checks cover Warn, mid-sprint
summary and sprint close with their eligibility gates. Deploy only one
scheduler (`LOCAL_CRON=false` when Functions is active).

Live mode uses delegated Graph device-code authentication for SharePoint,
separate from inbound request authentication and MCP authorization. The
file-backed MSAL cache is unencrypted and intended for local development.

Standup response fan-in, cutoff timers, completed daily guard and Q&A history
are process-local. SharePoint records do not make those maps a distributed
lock or reconstruct them after restart. Sprint-close delivery has a separate
durable per-sprint claim in live mode; `force` bypasses timing, not that guard.
Mock services and their changes are in-memory and reset with the process.
Live standup startup checks existing SharePoint session records before
fanout, but that check is not atomic across processes. Ambiguous failed
finalization remains `summarizing` for operator review, not automatic retry.
Loading
Loading